Vue2.x(含组件)主流程源码笔记(七):mount 阶段之生成 dom
vnode 渲染为真实 dom
接上文,vm._render
通过 render
生成 vnode
后,然后执行 vm._update(vm._render(),hydrating)
来首次渲染成真实 dom
,里面执行:
1 | if (!prevVnode) { |
此处为初始渲染,执行第一个分支 vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
即执行 patch
。
patch
中因为 oldVnode
即 vm.$el
是真实节点,则 oldVnode
替换为空 vnode
,然后执行:
1 | createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm)); |
createElm
createElm
主要逻辑:
- 是组件
vnode
,则在createComponent
处理 - 不是组件
vnode
- 是元素节点的
vnode
,则创建该标签并设置css scope
,然后通过createChildren
循环构建子vnode
,然后触发invokeCreateHooks
处理标签的属性 - 是注释节点的
vnode
,则创建注释节点 - 其他情况就创建文件节点
- 最后将创建的真实节点插入到根节点里
- 是元素节点的
createChildren
1 | createChildren(vnode, children, insertedVnodeQueue); |
createChildren
标志开始构建子 vnode
。方法里先通过 checkDuplicateKeys
检查 key
是否重复,然后执行:
1 | for (var i = 0; i < children.length; ++i) { |
即递归对每一个子 vnode
执行 createElm
。
invokeCreateHooks
1 | if (isDef(data)) { |
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
1 | function invokeInsertHook(vnode, queue, initial) { |
暂时不考虑服务端渲染,invokeInsertHook
钩子在 patch
方法的最后执行。如果是新建的组件实例 vnode
(如未挂载、组件实例)且有父 vnode
,则将 queue
即之前的 insertedVnodeQueue
队列存到父 vnode
上的 data.pendingInsert
上,在 initComponent
时,会把其 push
到 insertedVnodeQueue
。
如果不是,则就依次调用每个组件 vnode
的 insert
方法,如果组件还未 mounted
,则触发 mounted
钩子,如果是 keepAlive
包裹的组件,则执行:
1 | if (vnode.data.keepAlive) { |
分析 demo
对于本例:
1 | <div id="main"> |
除开 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
时安装的钩子函数,故执行:
1 | i(vnode, false /* hydrating */); //其中 i 为 componentVNodeHooks.init |
实例化子组件
执行 componentVNodeHooks.init
,方法里先判断如果存在已被 keep-alive
缓存的组件实例 vnode.componentInstance
,则调用 componentVNodeHooks.prepatch
直接 updateChildComponent
更新子组件实例即可,就不用去初始化和装载子组件,即不会走一系列常规生命周期钩子。否则执行:
1 | 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 钩子
,然后执行:
1 | if (vm.$options.el) { |
因 vm.$options.el
不存在,所以子组件的 _init
的执行结束(即子组件实例化完成),将子组件实例赋给 vnode.componentInstance
即 child
。
构建子组件
回到 componentVNodeHooks.init
继续执行:
1 | child.$mount(hydrating ? vnode.elm : undefined, hydrating); |
执行 child.$mount
即原型链上的 Vue
的方法 $mount
。因为存在 render
,故触发 App beforeMount 钩子
后,直接进入 mount
阶段。
前面已分析,在实例化 渲染 watcher
里触发 updateComponent
先执行 vm._render
生成 vnode
:
子组件 render 函数
1 | var render = function () { |
生成 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
方法里继续执行:
1 | if (isDef(vnode.componentInstance)) { |
initComponent
1 | function initComponent(vnode, insertedVnodeQueue) { |
在 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
继续执行:
1 | callHook(vm, 'mounted'); |
触发生命周期钩子 mounted
。在这之后,异步组件才加载结束,开始构建异步组件的生命周期(上一篇文章的异步组件第二阶段已分析),构建完成后,Vue
初始化全部完成。
整个过程中,生命周期顺序为:
1 | // 开始加载 Vue ↓↓ |
本章小结
- 本章介绍了
vue
执行的Mount
阶段中的通过vnode
渲染为真实dom
部分。 - 同时分析了组件里子组件,孙组件的渲染过程以及如何将渲染结果
dom
添加到对应的组件vnode
上。 - 分析了
Vue
的整体生命周期。