源码笔记(七):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的整体生命周期。