vue源码阅读五:虚拟DOM是如何渲染成真实的DOM的?(下)

2,342 阅读3分钟

上一篇:vue源码阅读四:虚拟DOM是如何渲染成真实的DOM的?(上)

前言

上文中讲了如何将普通的虚拟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方法 。 而hookinit 是什么时候挂载到vnode.data 上的呢。
在生成组件类型的虚拟DOMcreateComponent 方法中,有这样一个函数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.hookcomponentVNodeHooks的钩子函数进行合并。而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 实例,并将结果赋值给 childvnode.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属性,所以没有使用这里的挂载,而是在 componentVNodeHooksinit 中使用 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方法中,oldVnodeundefined的。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,然后插入到组件的父元素中。至此,组件的渲染也就讲完了。

总结

一图胜千言。