автор:Сюй จุ๊บ, несанкционированное воспроизведение запрещено.
предисловие
предыдущий пост«Исходный код Vue3 (1)»кратко представленVue3
Структура исходного кода, также изучайте исходный кодVue3
Основание также является реактивным. На этот раз давайте узнаем о другом ключевом компоненте и изучим его.Vue3
Инициализация компонента и процесс его рендеринга. Если есть какие-то неточности или упущения, просьба исправить и дополнить.
текст
Помните, что я упоминал в предыдущем постеVue3
Приложение изначальное?
createApp(App).mount('#app')
В прошлый раз мы узналиcreateApp(App)
Благодаря процессу закрытия и каррирования мы можем иметь дело с различными сценариями и платформами, создавать и возвращать конкретные экземпляры приложения, поэтому на этот раз наше исследование начинается сmount('#app')
Для начала разберитесь с процессом начального рендеринга.
метод крепления
Оглядываясь назад на содержание предыдущей статьи, мы обнаружили, что в исходном коде есть два основных определения.mount
методы соответственно:
- runtime-dom/src/index.ts переписан для веб-платформы браузера
mount
метод
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
// normallizeContainer 这个方法顾名思义统一容器,mount参数可能是DOM对象或者选择器
// 如果是选择器就取到对应DOM
const container = normalizeContainer(containerOrSelector)
if (!container) return
// 这里app._component就是我们通过 rootComponent 参数,传入打包编译过的 App 组件(图1)
const component = app._component
// 如果我们传入的组件没有定义render,没有模版,那就取DOM里面原本内容当作模版
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// 这里会清除DOM里原有的内容
container.innerHTML = ''
// 执行之前暂存的基础的 mount 方法
const proxy = mount(container)
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
return proxy
}
фигура 1:
По коду и комментариям внутри метод перезаписи можно разделить на несколько шагов: 1. Получить DOM-контейнер 2. Оценить входящее приложение корневого компонента 3. Выполнить стандартныйmount
метод.
- runtime-core/src/apiCreateApp.ts, это стандартный кроссплатформенный компонент в экземпляре приложения.
mount
метод
mount(rootContainer: HostElement, isHydrate?: boolean): any {
// app应用是否已经被挂载
if (!isMounted) {
// 1. 创建VNode 这里 rootComponent 就是 createApp(App) 传入的 App 组件
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// app应用实例存储上下文,主要有 app应用实例本身,各类设置项,配置项
vnode.appContext = context
if (isHydrate && hydrate) {
// 服务端渲染相关
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 2. render 渲染 VNode
// 这里的render再上一篇文章有提到 ensureRenderer 创建出来的
render(vnode, rootContainer)
}
isMounted = true
// 存储DOM容器
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
// ...
return vnode.component!.proxy
} else if (__DEV__) {
// ...
}
},
стандарт можно посмотретьmount
Метод в основном состоит из следующих шагов: 1. Создание VNode 2. Рендеринг VNode как настоящий DOM
резюме
На данный момент мы знаемmount
Что примерно делает метод.
- normalizeContainer получает контейнер DOM
- createVNode, согласно входящему компоненту приложения, создать VNode
- визуализировать VNode и смонтировать его в DOM-контейнере
- Возвращает прокси для VNode.component
Давайте посмотрим, что связано с VNode.
Создание и рендеринг VNode
Я полагаю, что все знакомы с VNode.Проще говоря, он абстрагирует DOM и другие вещи через объекты JavaScript. Если вы спросите о преимуществах в интервью, вы обязательно упомянете следующие моменты: 1. Вам не нужно часто менять DOM, 2. Кроссплатформенность, обеспечиваемая абстракцией, и 3. Преимущество в производительности при работе с VNode. JS по сравнению с прямым управлением DOM. Но недавно прочитав некоторые статьи, я подумал, что третье преимущество не является абсолютным.Для компонентов с большим объемом данных, таких как Дерево и Таблица, процесс зацикливания по под-VNode рендеринга занимает много времени.В итоге, по-прежнему необходимо манипулировать DOM, страница может даже застрять.
Возвращаясь к теме, посмотрите на следующий пример
App.vue
<template>
<HelloWorld msg="Hello Vue 3.0 + Vite" />
<p>{{ showText }}</p>
</template>
HelloWorld.vue
<template>
<div>{{ msg }}</div>
</template>
Создать виртуальный узел
существуетVue3
, есть много VNodes, представляющих разные категории, например, в приведенном выше примере.HelloWorld
Компоненты VNode, общие элементы VNodep
.
В частности, давайте рассмотрим метод создания VNode.createVNode
, код немного длиннее, старый метод заключается в том, чтобы закомментировать контент, который не волнует этот процесс.
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false
): VNode {
if (!type || type === NULL_DYNAMIC_COMPONENT) {
type = Comment
}
if (isVNode(type)) { // 如果是VNode,直接clone,这里就是通过type的__v_isVNode属性判断的
// createVNode receiving an existing vnode. This happens in cases like
// <component :is="vnode"/>
// #2078 make sure to merge refs during the clone instead of overwriting it
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
return cloned
}
// class component normalization.
if (isClassComponent(type)) { // class组件
type = type.__vccOpts
}
// class & style normalization.
if (props) {
// ...
}
// 给组件类型增加一个编码标示
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT // 1 dom element
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE //128 suspense vue3中新增的组件
: isTeleport(type)
? ShapeFlags.TELEPORT // 64 teleport 也是vue3中新增
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT // 4 状态组件
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT // 2 函数组件
: 0
// ...
const vnode: VNode = {
__v_isVNode: true,
[ReactiveFlags.SKIP]: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
children: null,
component: null,
shapeFlag
// ...
}
/** 标准化子节点
* 这里会给不同类型的children编码标示type 8: 文本; 16:数组; 32:slots;同时也转成对应类型。
* 同时会因children类型不同,修改VNode的 shapeFlag,为之后挂载使用
**/
normalizeChildren(vnode, children)
// normalize suspense children
//...
return vnode
}
Давайте посмотрим на процесс выполнения приведенного выше кода на этом примере.
- Определите, является ли это компонентом VNode, Class, и, если есть реквизиты, стандартизируйте преобразование класса и стиля.
- Определяем тип компонента и вычисляем оценку, получаем 4
- Создать виртуальный узел
- Стандартизируйте дочерние узлы, когда компонент приложения передается, дочерние узлы имеют значение null
- Вернуться к узлу
На данный момент мы получили VNode, созданный компонентом App:
визуализировать VNode
Давайте взглянем render(vnode, rootContainer)
, как визуализировать VNode.
В предыдущей статье мы также узналиrender
метод,baseCreateRenderer
Передавая rendererOptions разных платформ, можно создавать средства визуализации для разных платформ.
render
// runtime-core/src/renderer.ts
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
// 存下 vnode于dom容器上
container._vnode = vnode
}
Видно, что если входящий VNode пуст, а в текущем DOM-контейнере есть VNode, выполнить размонтирование, чтобы уничтожить компонент, в противном случае исправить входящий VNode. Тогда мы понимаемpatch
реализация.
patch
const patch: PatchFn = (
n1, // n1 代表旧节点
n2, // n2 代表新节点
container,
anchor = null,
parentComponent = null,parentSuspense = null,isSVG = false,optimized = false
) => {
// 如果有旧VNode,且不一样,umount销毁旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
// 先通过type来判断选择处理方法
switch (type) {
case Text:
// 文本
processText(n1, n2, container, anchor)
break
case Comment:
// 注释
processCommentNode(n1, n2, container, anchor)
break
case Static:
// 静态
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
// 碎片化,这也是Vue3新支持的多根节点
processFragment(/** ... **/)
break
default:
// 如果type都不满足,使用shapeFlag 编码判断
if (shapeFlag & ShapeFlags.ELEMENT) {
// dom元素
processElement(/** ... **/)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 组件本次初次渲染会走到这里
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
// 之后都是Vue3 里面新增两种组件
} else if (shapeFlag & ShapeFlags.TELEPORT) {
//
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
}
}
фактическиpatch
Наиболее важной логикой является выбор того, как поступать с компонентами через type и shapeFlag vnode.
Так как мы рендерим в первый раз, n1 пуст, и создается компонент AppVNode
изshapeFlag
на 4ShapeFlags.STATEFUL_COMPONENT
, так пойдет наShapeFlags.COMPONENT
условие, выполнитьprocessComponent
метод. Тогда взгляните на этот метод.
processComponent
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
if (n1 == null) {
// 如果没有旧节点
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { // 512
// 如果是 keep-alive 组件
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
// 执行挂载组件
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
// 如果n1 n2 都有则执行更新
updateComponent(n1, n2, optimized)
}
}
Основная логика этого метода заключается в монтировании компонентов по наличию новых и старых узлов.mountComponent
, ещеupdateComponent
Обновление компонентов.
Давайте посмотрим на выполнение этого начального рендеринга.mountComponent
mountComponent
const mountComponent: MountComponentFn = (
initialVNode, // 初始VNode 也就是App组件生成的VNode
container, // #app Dom容器
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 创建组件实例
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// inject renderer internals for keepAlive
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
// 设置实例 初始化 props,slots 还有Vue3新增的composition API
setupComponent(instance)
// ...
// effect 上一篇说到的副作用函数
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
Основная логика монтажа компонента VNode такова:createComponentInstance
Создание компонентовinstance
пример,setupComponent
установить компоненты,setupRenderEffect
Выполнение функции рендеринга с побочными эффектами.
createComponentInstance
Главное создать и вернутьinstance
пример, посмотримinstance
На что это похоже.
const instance: ComponentInternalInstance = {
uid: uid++,
vnode,
type,
parent,
appContext,
root: null!, // to be immediately set
next: null,
subTree: null!, // will be set synchronously right after creation
update: null!, // will be set synchronously right after creation
render: null,
proxy: null,
withProxy: null,
effects: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
accessCache: null!,
renderCache: [],
// local resovled assets
components: null,
directives: null,
// resolved props and emits options
//
// emit
emit: null as any, // to be set immediately
emitted: null,
// state
ctx: EMPTY_OBJ,
data: EMPTY_OBJ,
props: EMPTY_OBJ,
// ...
// suspense related
// ...
// lifecycle hooks
// 以下是 组件生命周期相关的属性
isMounted: false,
isUnmounted: false,
isDeactivated: false,
bc: null, // beforeCreate
c: null, // created
// ...
}
instance
Есть много атрибутов объекта, которые будут использоваться в определенных сценариях, а позжеsetupComponent
Метод также заключается в установке инициализацииinstance
свойства, такие как инициализацияprops
, slots
Существует также функция настройки, которая запускает новый Vue3.
Поскольку он включает в себя новый API композиции и функцию настройки в Vue3, вы можете вырыть дыру, чтобы изучить этот контент отдельно.
После того, как экземпляр создан и установлен, последним шагом является установка и запуск функции побочного эффекта рендеринга.setupRenderEffect
.
setupRenderEffect
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// 创建响应式的副作用render函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance // 生命周期, beforemounted , mounted
// bm 生命周期 及 hook 执行
if (bm) {
invokeArrayFns(bm)
}
// ..
// 渲染组件生成 subTree VNode
const subTree = (instance.subTree = renderComponentRoot(instance))
if (el && hydrateNode) {
// ...
} else {
// 把 subTree 挂载到Dom容器中
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
initialVNode.el = subTree.el
}
// 生命周期 mounted hook 执行
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// ...
instance.isMounted = true
} else {
// updateComponent
// This is triggered by mutation of component's own state (next: null)
// OR parent calling processComponent (next: VNode)
}
}, prodEffectOptions)
}
Ознакомьтесь с содержанием предыдущей статьиeffect
Функция не должна быть всем незнакома, запускайтеcomponentEffect
Инициировать сбор зависимостей, собрать эту функцию эффекта и повторно выполнить ее при изменении данных компонента.effect
в функцииcomponentEffect
метод.
componentEffect
Основная логика состоит в том, чтобы сгенерировать VNode поддерева, а затем смонтировать поддерево.
renderComponentRoot
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
const {
type: Component,
vnode,
proxy,
withProxy,
props,
propsOptions: [propsOptions],
slots,
attrs,
emit,
render, // 这里render 是 .vue 编译后的render函数
renderCache,
data,
setupState,
ctx
} = instance
let result
currentRenderingInstance = instance
try {
let fallthroughAttrs
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
const proxyToUse = withProxy || proxy
// 本次例子中 这里会循环创建 Helloworld, p标签 VNode
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
fallthroughAttrs = attrs
} else {
// functional
} catch (err) {
// ...
}
currentRenderingInstance = null
return result
}
subTree
что это такое? Например, первый пример компонента приложения: initialVNode
,subTree
Это виртуальный узел, сгенерированный структурой в шаблоне компонента приложения.children
собственность HelloWorld
компонент VNode,p
Метка VNode.
Компоненты приложенияinitialVNode
изchidren
внутри, согласноHelloWorld
Метка, сгенерированная VNode, дляHelloWorld
Внутренняя структура DOM компонента initialVNode
, а VNode, сгенерированный его внутренней структурой DOM,subTree
.
На следующем рисунке показана скомпилированная функция рендеринга HelloWorld.vue в примере.
Это поддерево приложения
Вы можете видеть, что в нем есть детиHelloworld
, p
Метка VNode.
назадsetupRenderEffect
метод созданияsubTree
После этого мы возвращаемся к нашему предыдущему процессу исправления, чтобы определить, что делать с входящим VNode, и так далее, и так далее, пока мы не исправим реальные элементы DOM, комментарии и другие VNode.
Не знаю, заметили ли вы, что в шаблоне App.vue в стартовом примере отсутствует корневой узел, что такжеVue3
Medium Новая поддерживаемая функция определенно нужна в Vue2.div
ПучокHelloWorld
, p
завернутые в этикетки.
Итак, в нашем примере компонент APPsubTree
анализируется как type
дляSymbol(Fragment)
Vузел.
назадpatch
метод см.processFragment
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
// 没有根节点,要确认分配在何处
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
// ...
if (n1 == null) {
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
// 走到这里children一定会是数组
mountChildren(
n2.children as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {}
}
надhostCreateText
, hostInsert
Все создано, как мы сказали в предыдущем постеrender
входящийrendererOptions
, который содержит DOM API браузера, props. НапримерhostCreateText
На самом деле этоdocument.createTextNode
,hostInsert
то естьparent.insertBefore(*child*, *anchor* || null)
.
processFragment
После того, как место определено,mountChildren
иметь дело сchildren
Массив VNodes.
mountChildren
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized,
start = 0
) => {
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
// patch每一个VNode
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
}
можно увидетьmountChildren
пройдетchildren
, patch
каждый VNode к текущемуcontainer
Вниз.
назад сноваpatch
, Тогда давайте посмотрим, как это обрабатывается, если это узел DOM VNode.
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
//
}
}
Процесс обработки компонентов почти такой же, как выполнять монтирование или обновление в зависимости от того, есть старые узлы или нет.
mountElement
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const {
type,
props,
shapeFlag,
transition,
scopeId,
patchFlag,
dirs
} = vnode
// ...
// 调用传入的API创建DOM元素
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is
)
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 8
// 如果是子节点文本 创建文本
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 16
// 如果是数组,回到mountChildren遍历继续patch子节点
// 注意这里传入的 container 已经是刚刚创建的 el DOM元素,这样就创建了父子关系
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
optimized || !!vnode.dynamicChildren
)
}
if (dirs) {
// 调用指令相关的生命周期处理
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// 如果有DOM的 props,例如原生的class style,自定义的prop等
if (props) {
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
// ...
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
/** 把创建的el DOM挂载到 contanier容器上
* 初次渲染container是 #app 容器,但是之后就是对应的父级DOM容器了
**/
hostInsert(el, container, anchor)
// ...
}
Видно, что основная логика обработки и монтирования DOM-узлов заключается в первом вызовеhostCreateElement
создать ДОМ,hostCreateElement
На самом деле он вызывает браузерdocument.createElement
. Затем определите, является ли дочерний узел обработки текстом или массивом. Затем обработайте собственные или пользовательские свойства модели DOM. последний звонокinsert
Монтируется в DOM-контейнер.
кHelloWorld
Внутри компонентаdiv
Например, егоchildren
просто абзац, который мы прошлиprop
входящий текст, поэтому звонитеhostSetElementText
: el.textContent = *text*
Просто вставьте текст.
У некоторых могут быть сомненияdiv
Как VNode of shapeFlag будет 9, помнитеcreateVNode
внутри методаnormalizeChildren
действовать? Он изменяет значение shapeFlag в зависимости от того, является ли тип дочерних элементов массивом, текстом или слотом.
резюме
Глядя на процесс рендеринга через код, вы не чувствуете себя очень запутанным, вы можете использовать блок-схему, чтобы понять это.
конец
Спасибо за прочтение, на днях智云健康大前端团队
участие掘金人气团队评选活动
. Если тебе хорошо, то приходиГолосуйте за насБар!
Всего сегодня можно отдать 12 голосов: 4 в Интернете, 4 в приложении и 4 на акции. Спасибо за вашу поддержку, мы создадим больше технических статей в 2021 году~~~
Ваша поддержка - наша самая большая мотивация~