Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yangrenmu opened this issue Dec 27, 2019 · 0 comments
Labels

Comments

@yangrenmu
Copy link
Owner

上一篇: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,然后插入到组件的父元素中。至此,组件的渲染也就讲完了。

总结

一图胜千言。

@yangrenmu yangrenmu added the vue label Dec 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant