前言
上文中讲了如何将普通的虚拟DOM转为真实的DOM,本文中则继续讲如何将组件类型的虚拟DOM转为真实的DOM。
组件类型的Vnode
// 若是组件节点,则调用 createComponent 方法
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
复制代码
如果是组件类型的Vnode,则在生成DOM时,调用的是createComponent方法。
createComponent
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm){
let i = vnode.data
if (isDef(i)) {
// 当 vnode 上有 hook 和 init 时,将 i = vnode.data.init
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 相当于 init(vnode, false)
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
// 先放在这
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
return true
}
}
}
复制代码
我们可以看到,先是判断 vnode.data 上是否有 hook 和 init,如果有的话,则执行init方法 。
而hook和init 是什么时候挂载到vnode.data 上的呢。
在生成组件类型的虚拟DOM的 createComponent 方法中,有这样一个函数installComponentHooks(data),这个函数主要的代码如下:
// 将 data.hook 与 componentVNodeHooks 的钩子进行合并
function installComponentHooks(data: VNodeData){
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
复制代码
所以installComponentHooks函数的主要作用是将data.hook与componentVNodeHooks的钩子函数进行合并。而componentVNodeHooks的钩子函数又有哪些呢。
componentVNodeHooks
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): ?boolean {
...
// 创建组件的实例
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
},
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
...
},
insert(vnode: MountedComponentVNode) {
...
},
destroy(vnode: MountedComponentVNode) {
...
}
}
复制代码
componentVNodeHooks 里面有四个钩子,我们先主要看看 init 这个钩子 ,后面三个用到时候,我们再去详细的看。经过钩子的合并,vnode.data 上就有 hook 和 init了。
在 init 这个钩子内,调用 createComponentInstanceForVnode方法创建vue 实例,并将结果赋值给 child 和 vnode.componentInstance。最后调用 child.$mount来渲染组件。详细看下createComponentInstanceForVnode。
createComponentInstanceForVnode
export function createComponentInstanceForVnode(vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state): Component{
const options: InternalComponentOptions = {
_isComponent: true, // 组件的标志
_parentVnode: vnode,
parent
}
...
// 创建一个新的 vue 的实例
return new vnode.componentOptions.Ctor(options)
}
--------------------------------------------------------------------
// tips:在该系列第三篇中,我们介绍了如何生成组件类型的虚拟DOM,其中有如下代码:
// componentOptions 中的 Ctor 则是 Vue 的子类,拥有 Vue 的完整的功能
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, // 对应tag
data, // 父组件自定义事件和patch时用到的方法
undefined, // children
undefined, // text
undefined, // 节点
context, // 当前实例
{ Ctor, propsData, listeners, tag, children }, // 对应componentOptions属性
asyncFactory
)
复制代码
在createComponentInstanceForVnode方法的最后,可以看到,调用new vnode.componentOptions.Ctor(options)生成新的vue实例,相当于执行 new Vue() ,接着又会执行最开始的 _init 方法。回顾下_init中的代码。
Vue.prototype._init = function (options?: Object){
// ...
if (options && options._isComponent) {
// 优化合并组件内部选项
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 组件没有 el,不会执行 vm.$mount。所以在 componentVNodeHooks 的 init 中
// 使用 child.$mount 来进行组件虚拟 DOM 的构建和渲染
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
复制代码
再次执行_init方法时,首先使用initInternalComponent优化合并组件内部选项。然后由于没有 vm.$options.el属性,所以没有使用这里的挂载,而是在 componentVNodeHooks 的 init 中使用 child.$mount 来进行组件虚拟 DOM 的构建和渲染。之后就是执行组件的_render 方法得到组件内部元素的虚拟 DOM,接着是_update方法渲染虚拟 DOM。
export function createPatchFunction(backend){
...
return function patch(oldVnode, vnode, hydrating, removeOnly){
if (isUndef(oldVnode)) {
createElm(vnode, insertedVnodeQueue)
}
}
...
}
复制代码
在渲染的过程中,由于child.$mount(undefined)里传的是undefined,所以在createPatchFunction方法中,oldVnode 是undefined的。createEle 方法中第三个参数parentElm就么得了。所以组件内的真实DOM创建好了,在这里也木有立即插入。
当组件内嵌套组件时,在渲染时,遇到组件会再次执行init(),整个过程是递归执行的。当全部的init()执行完后,后面的代码如下:
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm){
let i = vnode.data
if (isDef(i)) {
// 当 vnode 上有 hook 和 init 时,将 i = vnode.data.init
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 相当于 init(vnode, false)
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
// 将组件内真实 DOM 赋值给 vnode.elm
initComponent(vnode, insertedVnodeQueue)
// 插入组件内真实的 DOM
insert(parentElm, vnode.elm, refElm)
return true
}
}
}
------------------------------------------------------------
function initComponent(vnode, insertedVnodeQueue){
// 将组组件内元素的只是 DOM 赋值给 vnode.elm
vnode.elm = vnode.componentInstance.$el
...
}
复制代码
这部分代码主要作用是将组件内部元素的真实DOM赋值给 vnode.elm,然后插入到组件的父元素中。至此,组件的渲染也就讲完了。
总结
一图胜千言。