源码笔记(七):mount 阶段生成 dom
vnode 渲染为真实 dom
接上文,vm._render
通过 render
生成 vnode
后,然后执行 vm._update(vm._render(),hydrating)
来首次渲染成真实 dom
,里面执行:
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
此处为初始渲染,执行第一个分支 vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
即执行 patch
。
patch
中因为 oldVnode
即 vm.$el
是真实节点,则 oldVnode
替换为空 vnode
,然后执行:
createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm));
createElm
createElm
主要逻辑:
- 是组件
vnode
,则在createComponent
处理 - 不是组件
vnode
- 是元素节点的
vnode
,则创建该标签并设置css scope
,然后通过createChildren
循环构建子vnode
,然后触发invokeCreateHooks
处理标签的属性 - 是注释节点的
vnode
,则创建注释节点 - 其他情况就创建文件节点
- 最后将创建的真实节点插入到根节点里
- 是元素节点的
createChildren
createChildren(vnode, children, insertedVnodeQueue);
createChildren
标志开始构建子 vnode
。方法里先通过 checkDuplicateKeys
检查 key
是否重复,然后执行:
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
即递归对每一个子 vnode
执行 createElm
。
invokeCreateHooks
if (isDef(data)) {
// `data` 为:`{ref: "btn", staticClass: "side", on: {click: ƒ}}`。
invokeCreateHooks(vnode, insertedVnodeQueue);
}
invokeCreateHooks
处理该 vnode
上的 data
属性,里循环调用 cbs.create
数组里的 8 个方法如 updateAttrs,updateClass,updateDOMListeners
等,做一些 style,class,event,$refs
等相关的处理。如果是组件 vnode
且有 insert
方法则将该组件 vnode
push
到 insertedVnodeQueue
。
insertedVnodeQueue
记录子节点组件创建顺序的队列。每创建一个组件实例就会往这个队列中插入当前的组件节点 VNode
, 当整个 VNode
对象全部转换成为真实的 DOM
树时,会依次调用这个队列中的 VNode hook
的 insert
方法。
invokeInsertHook
function invokeInsertHook(vnode, queue, initial) {
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue;
} else {
for (var i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]);
}
}
}
暂时不考虑服务端渲染,invokeInsertHook
钩子在 patch
方法的最后执行。如果是新建的组件实例 vnode
(如未挂载、组件实例)且有父 vnode
,则将 queue
即之前的 insertedVnodeQueue
队列存到父 vnode
上的 data.pendingInsert
上,在 initComponent
时,会把其 push
到 insertedVnodeQueue
。
如果不是,则就依次调用每个组件 vnode
的 insert
方法,如果组件还未 mounted
,则触发 mounted
钩子,如果是 keepAlive
包裹的组件,则执行:
if (vnode.data.keepAlive) {
if (context._isMounted) {
//已装载好,说明在更新,将 componentInstance push 到 activatedChildren
//后面组件更新结束后,在 flushSchedulerQueue 执行 callActivatedHooks,即遍历整个activatedChildren队列 执行 activateChildComponent
queueActivatedComponent(componentInstance);
} else {
//触发子元素的 activated 钩子和自己的 activated 钩子。同理,keep-alive 的 deactivated 钩子原理一致
activateChildComponent(componentInstance, true);
}
分析 demo
对于本例:
<div id="main">
<!-- 节点 1 -->
<Bpp></Bpp>
<!-- 节点 2 -->
<div v-on:click="plus">info.name:{{info.name}},计算属性:{{compute}}</div>
<!-- 节点 3 -->
<App name="one" v-bind:num="info.age"></App>
<!-- 节点 4 -->
<div v-on:click="hide">====点击让第二个App组件卸载====</div>
<!-- 节点 5 -->
<App name="two" v-if="isShow"></App>
</div>
除开 4 个空节点和一个异步组件占位的空注释节点,还有 4 个子节点。
接下来逐个分析创建流程:
- 创建 4 个空节点,即在
createElm
里直接执行createTextNode
生成空的文本节点后插入到对应的位置,下面的节点分析将略过空节点; - 创建第一个节点(称为节点 1,下一个就为节点 2,以此类推)异步占位节点,即在
createElm
里直接执行createComment
生成空的注释节点后插入到对应的位置; - 创建节点 2,同前面逻辑一样走
createElm -> createChildren -> createElm -> createChildren ···
,递归执行createElm
判断到tag
为空,则执行createTextNode
创建文本节点info.name:korey,计算属性:29
并调insert
插入到父节点。然后回到节点 2 的createElm
继续执行invokeCreateHooks
,然后调insert
插入到父节点即根组件的div
,此时生成的dom
节点都赋值在各自vnode.elm
下; - 创建节点 3,
vnode
为App
组件节点,在createElm
里走createComponent
方法。下面单独分析; - 创建节点 4,与节点 2 一致;
- 创建节点 5,与节点 3 一致;
组件 vnode 渲染为真实 dom
组件节点在 createElm
里走 createComponent
。因为组件 vnode.data
有 hook.init
等渲染 vnode
时安装的钩子函数,故执行:
i(vnode, false /* hydrating */); //其中 i 为 componentVNodeHooks.init
实例化子组件
执行 componentVNodeHooks.init
,方法里先判断如果存在已被 keep-alive
缓存的组件实例 vnode.componentInstance
,则调用 componentVNodeHooks.prepatch
直接 updateChildComponent
更新子组件实例即可,就不用去初始化和装载子组件,即不会走一系列常规生命周期钩子。否则执行:
var child = (vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance));
createComponentInstanceForVnode
用于创建 component
实例。
方法里执行:new vnode.componentOptions.Ctor(options)
,即实例化 VueComponent
,内部执行 this._init(options)
即原型链上的 Vue
的方法 _init
,到此,App
子组件开始走 vue
的 init
流程(具体与 Vue
实例化差异可以参考第一章分析),触发 App beforeCreate 钩子->App created 钩子
,然后执行:
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
因 vm.$options.el
不存在,所以子组件的 _init
的执行结束(即子组件实例化完成),将子组件实例赋给 vnode.componentInstance
即 child
。
构建子组件
回到 componentVNodeHooks.init
继续执行:
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
执行 child.$mount
即原型链上的 Vue
的方法 $mount
。因为存在 render
,故触发 App beforeMount 钩子
后,直接进入 mount
阶段。
前面已分析,在实例化 渲染 watcher
里触发 updateComponent
先执行 vm._render
生成 vnode
:
子组件 render 函数
var render = function () {
var _vm = this;
var _h = _vm.$createElement;
var _c = _vm._self._c || _h;
return _c(
'div',
{ attrs: { id: 'app' } },
[
_c('div', [_vm._v(_vm._s(_vm.num ? 'num:' + _vm.num : ''))]),
_vm._v(' '),
_c('Child', { staticClass: 'example' }),
],
1,
);
};
生成 vnode
后,执行 vm._update
渲染成真实 dom
。在 patch
方法里,由于 oldVnode
即 vm.$el
不存在,执行 createElm->createChildren->··
递归生成每一个 vnode
及其子 vnode
对应的真实 dom
。
构建孙组件
在子组件 App
的 render
函数里,创建孙组件 Child
。与子组件流程一致,他同样会经历 App
组件的构建逻辑(触发 Child beforeCreate->Child created->Child beforeMount
钩子),执行 vm._update
渲染成真实 dom
, 通过 createElm->createChildren->··
递归在其各层 vnode
里得到对应的 vnode.elm
。
在孙组件 Child
里的根节点时(createElm
里),由于没有父节点 parentElm
,所以执行 insert
时,不会插入节点。
然后回到 patch
触发 invokeInsertHook
(此时不存在 insertedVnodeQueue
),然后返回 vnode.elm
(vnode
为 Child
组件根 vnode
) 赋给 vm.$el
(vm
为 Child
组件实例)。回到 mountComponent
中返回孙组件实例, Child
构建完毕。
真实 dom 插入到文档
孙组件 dom 插入到子组件 dom
回到 createComponent
方法里继续执行:
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true;
}
initComponent
function initComponent(vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
vnode.data.pendingInsert = null;
}
vnode.elm = vnode.componentInstance.$el;
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
setScope(vnode);
} else {
registerRef(vnode); // make sure to invoke the insert hook
insertedVnodeQueue.push(vnode);
}
}
在 initComponent
里, vnode
为 Child
组件 vnode
。若vnode.data.pendingInsert
存在(存在说明有子组件 vnode
push
进入)则与 insertedVnodeQueue
合并,然后将 Child
组件实例生成的真实 dom
节点 vnode.componentInstance.$el
赋给 Child
组件 vnode.elm
上。
然后判断如果 Child
组件实例 vnode
有标签,则执行 invokeCreateHooks
(此处会把 Child
组件 vnode
push
到 insertedVnodeQueue
)和 setScope
。
initComponent
执行完成后,此时调用 insert
将 vnode.elm
插入到 App
组件对应的 dom
上。到此 Child
孙组件渲染结束。
子组件 dom 插入到根组件 dom
父级即 App
组件的 children
渲染循环也结束,然后回到 patch
触发 invokeInsertHook
将 insertedVnodeQueue
存入 vnode.parent.data.pendingInsert
,然后返回 vnode.elm
(vnode
为 App
组件根 vnode
) 赋给 vm.$el
(vm
为 App
组件实例)。回到 mountComponent
中返回子组件实例, App
构建完毕。
同 Child
的流程一致,回到 createComponent
方法执行了 initComponent
后,此时调用 insert
将 vnode.elm
插入到根组件对应的 dom
上。到此,App
组件渲染结束。
整体生命周期
根组件下的 createChildren
里继续循环 createElm
,遇到下一个 App
组件的构建 dom
跟之前的逻辑一样,其中构造函数复用。
待到根组件的 createChildren
里渲染循环也结束,在 createElm
里因为有 parentElm
为 body
节点,所以执行 insert
将整个父组件 vnode.elm
插入到 body
元素中。
回到 patch
方法里继续执行,删掉老的 dom
节点,然后触发 invokeInsertHook
,此时 insertedVnodeQueue
里包含之前按顺序 push
进去的 4 个组件 vnode
:
- 第一个孙组件
Child
- 第一个子组件
App
- 第二个孙组件
Child
- 第二个子组件
App
循环依次执行 insert
钩子,方法里会触发各组件生命周期钩子:mounted
。
然后 patch
方法返回 vnode.elm
赋给根实例 vm.$el
, vm._update
执行结束,实例化渲染 watcher
亦结束。
然后回到 mountComponent
继续执行:
callHook(vm, 'mounted');
触发生命周期钩子 mounted
。在这之后,异步组件才加载结束,开始构建异步组件的生命周期(上一篇文章的异步组件第二阶段已分析),构建完成后,Vue
初始化全部完成。
整个过程中,生命周期顺序为:
// 开始加载 Vue ↓↓
vue beforeCreate
vue created
vue beforeMount
// 开始加载第一个 App 组件 ↓↓
App beforeCreate
App one created
App one beforeMount
// 开始加载第一个 App 内的 Child 组件 ↓↓
Child beforeCreate
Child created
Child beforeMount
// 开始加载第二个 App 组件 ↓↓
App beforeCreate
App two created
App two beforeMount
// 开始加载第二个 App 内的 Child 组件 ↓↓
Child beforeCreate
Child created
Child beforeMount
// 调用 invokeInsertHook 依次触发各组件 mounted
// 第一个 App 内的 Child 组件 ↓↓
Child mounted
// 第一个 App 组件 ↓↓
App one mounted
// 第二个 App 内的 Child 组件 ↓↓
Child mounted
// 第二个 App 组件 ↓↓
App two mounted
// Vue加载完成 ↓↓
vue mounted
// 开始加载异步 Bpp 组件 ↓↓
vue beforeUpdate
async Bpp beforeCreate
async Bpp created
async Bpp beforeMount
async Bpp mounted
vue updated
本章小结
- 本章介绍了
vue
执行的Mount
阶段中的通过vnode
渲染为真实dom
部分。 - 同时分析了组件里子组件,孙组件的渲染过程以及如何将渲染结果
dom
添加到对应的组件vnode
上。 - 分析了
Vue
的整体生命周期。