• patch
    • createComponent
    • 总结

    patch

    通过前一章的分析我们知道,当我们通过 createComponent 创建了组件 VNode,接下来会走到 vm.update,执行 vm._patch 去把 VNode 转换成真正的 DOM 节点。这个过程我们在前一章已经分析过了,但是针对一个普通的 VNode 节点,接下来我们来看看组件的 VNode 会有哪些不一样的地方。

    patch 的过程会调用 createElm 创建元素节点,回顾一下 createElm 的实现,它的定义在 src/core/vdom/patch.js 中:

    1. function createElm (
    2. vnode,
    3. insertedVnodeQueue,
    4. parentElm,
    5. refElm,
    6. nested,
    7. ownerArray,
    8. index
    9. ) {
    10. // ...
    11. if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    12. return
    13. }
    14. // ...
    15. }

    createComponent

    我们删掉多余的代码,只保留关键的逻辑,这里会判断 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值,如果为 true 则直接结束,那么接下来看一下 createComponent 方法的实现:

    1. function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    2. let i = vnode.data
    3. if (isDef(i)) {
    4. const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    5. if (isDef(i = i.hook) && isDef(i = i.init)) {
    6. i(vnode, false /* hydrating */)
    7. }
    8. // after calling the init hook, if the vnode is a child component
    9. // it should've created a child instance and mounted it. the child
    10. // component also has set the placeholder vnode's elm.
    11. // in that case we can just return the element and be done.
    12. if (isDef(vnode.componentInstance)) {
    13. initComponent(vnode, insertedVnodeQueue)
    14. insert(parentElm, vnode.elm, refElm)
    15. if (isTrue(isReactivated)) {
    16. reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
    17. }
    18. return true
    19. }
    20. }
    21. }

    createComponent 函数中,首先对 vnode.data 做了一些判断:

    1. let i = vnode.data
    2. if (isDef(i)) {
    3. // ...
    4. if (isDef(i = i.hook) && isDef(i = i.init)) {
    5. i(vnode, false /* hydrating */)
    6. // ...
    7. }
    8. // ..
    9. }

    如果 vnode 是一个组件 VNode,那么条件会满足,并且得到 i 就是 init 钩子函数,回顾上节我们在创建组件 VNode 的时候合并钩子函数中就包含 init 钩子函数,定义在 src/core/vdom/create-component.js 中:

    1. init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    2. if (
    3. vnode.componentInstance &&
    4. !vnode.componentInstance._isDestroyed &&
    5. vnode.data.keepAlive
    6. ) {
    7. // kept-alive components, treat as a patch
    8. const mountedNode: any = vnode // work around flow
    9. componentVNodeHooks.prepatch(mountedNode, mountedNode)
    10. } else {
    11. const child = vnode.componentInstance = createComponentInstanceForVnode(
    12. vnode,
    13. activeInstance
    14. )
    15. child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    16. }
    17. },

    init 钩子函数执行也很简单,我们先不考虑 keepAlive 的情况,它是通过 createComponentInstanceForVnode 创建一个 Vue 的实例,然后调用 $mount 方法挂载子组件,先来看一下 createComponentInstanceForVnode 的实现:

    1. export function createComponentInstanceForVnode (
    2. vnode: any, // we know it's MountedComponentVNode but flow doesn't
    3. parent: any, // activeInstance in lifecycle state
    4. ): Component {
    5. const options: InternalComponentOptions = {
    6. _isComponent: true,
    7. _parentVnode: vnode,
    8. parent
    9. }
    10. // check inline-template render functions
    11. const inlineTemplate = vnode.data.inlineTemplate
    12. if (isDef(inlineTemplate)) {
    13. options.render = inlineTemplate.render
    14. options.staticRenderFns = inlineTemplate.staticRenderFns
    15. }
    16. return new vnode.componentOptions.Ctor(options)
    17. }

    createComponentInstanceForVnode 函数构造的一个内部组件的参数,然后执行 new vnode.componentOptions.Ctor(options)。这里的 vnode.componentOptions.Ctor 对应的就是子组件的构造函数,我们上一节分析了它实际上是继承于 Vue 的一个构造器 Sub,相当于 new Sub(options) 这里有几个关键参数要注意几个点,_isComponenttrue 表示它是一个组件,parent 表示当前激活的组件实例(注意,这里比较有意思的是如何拿到组件实例,后面会介绍。

    所以子组件的实例化实际上就是在这个时机执行的,并且它会执行实例的 _init 方法,这个过程有一些和之前不同的地方需要挑出来说,代码在 src/core/instance/init.js 中:

    1. Vue.prototype._init = function (options?: Object) {
    2. const vm: Component = this
    3. // merge options
    4. if (options && options._isComponent) {
    5. // optimize internal component instantiation
    6. // since dynamic options merging is pretty slow, and none of the
    7. // internal component options needs special treatment.
    8. initInternalComponent(vm, options)
    9. } else {
    10. vm.$options = mergeOptions(
    11. resolveConstructorOptions(vm.constructor),
    12. options || {},
    13. vm
    14. )
    15. }
    16. // ...
    17. if (vm.$options.el) {
    18. vm.$mount(vm.$options.el)
    19. }
    20. }

    这里首先是合并 options 的过程有变化,_isComponent 为 true,所以走到了 initInternalComponent 过程,这个函数的实现也简单看一下:

    1. export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
    2. const opts = vm.$options = Object.create(vm.constructor.options)
    3. // doing this because it's faster than dynamic enumeration.
    4. const parentVnode = options._parentVnode
    5. opts.parent = options.parent
    6. opts._parentVnode = parentVnode
    7. const vnodeComponentOptions = parentVnode.componentOptions
    8. opts.propsData = vnodeComponentOptions.propsData
    9. opts._parentListeners = vnodeComponentOptions.listeners
    10. opts._renderChildren = vnodeComponentOptions.children
    11. opts._componentTag = vnodeComponentOptions.tag
    12. if (options.render) {
    13. opts.render = options.render
    14. opts.staticRenderFns = options.staticRenderFns
    15. }
    16. }

    这个过程我们重点记住以下几个点即可:opts.parent = options.parentopts._parentVnode = parentVnode,它们是把之前我们通过 createComponentInstanceForVnode 函数传入的几个参数合并到内部的选项 $options 里了。

    再来看一下 _init 函数最后执行的代码:

    1. if (vm.$options.el) {
    2. vm.$mount(vm.$options.el)
    3. }

    由于组件初始化的时候是不传 el 的,因此组件是自己接管了 $mount 的过程,这个过程的主要流程在上一章介绍过了,回到组件 init 的过程,componentVNodeHooksinit 钩子函数,在完成实例化的 _init 后,接着会执行 child.$mount(hydrating ? vnode.elm : undefined, hydrating) 。这里 hydrating 为 true 一般是服务端渲染的情况,我们只考虑客户端渲染,所以这里 $mount 相当于执行 child.$mount(undefined, false),它最终会调用 mountComponent 方法,进而执行 vm._render() 方法:

    1. Vue.prototype._render = function (): VNode {
    2. const vm: Component = this
    3. const { render, _parentVnode } = vm.$options
    4. // set parent vnode. this allows render functions to have access
    5. // to the data on the placeholder node.
    6. vm.$vnode = _parentVnode
    7. // render self
    8. let vnode
    9. try {
    10. vnode = render.call(vm._renderProxy, vm.$createElement)
    11. } catch (e) {
    12. // ...
    13. }
    14. // set parent
    15. vnode.parent = _parentVnode
    16. return vnode
    17. }

    我们只保留关键部分的代码,这里的 _parentVnode 就是当前组件的父 VNode,而 render 函数生成的 vnode 当前组件的渲染 vnodevnodeparent 指向了 _parentVnode,也就是 vm.$vnode,它们是一种父子的关系。

    我们知道在执行完 vm._render 生成 VNode 后,接下来就要执行 vm._update 去渲染 VNode 了。来看一下组件渲染的过程中有哪些需要注意的,vm._update 的定义在 src/core/instance/lifecycle.js 中:

    1. export let activeInstance: any = null
    2. Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    3. const vm: Component = this
    4. const prevEl = vm.$el
    5. const prevVnode = vm._vnode
    6. const prevActiveInstance = activeInstance
    7. activeInstance = vm
    8. vm._vnode = vnode
    9. // Vue.prototype.__patch__ is injected in entry points
    10. // based on the rendering backend used.
    11. if (!prevVnode) {
    12. // initial render
    13. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    14. } else {
    15. // updates
    16. vm.$el = vm.__patch__(prevVnode, vnode)
    17. }
    18. activeInstance = prevActiveInstance
    19. // update __vue__ reference
    20. if (prevEl) {
    21. prevEl.__vue__ = null
    22. }
    23. if (vm.$el) {
    24. vm.$el.__vue__ = vm
    25. }
    26. // if parent is an HOC, update its $el as well
    27. if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    28. vm.$parent.$el = vm.$el
    29. }
    30. // updated hook is called by the scheduler to ensure that children are
    31. // updated in a parent's updated hook.
    32. }

    _update 过程中有几个关键的代码,首先 vm._vnode = vnode 的逻辑,这个 vnode 是通过 vm._render() 返回的组件渲染 VNode,vm._vnodevm.$vnode 的关系就是一种父子关系,用代码表达就是 vm._vnode.parent === vm.$vnode。还有一段比较有意思的代码:

    1. export let activeInstance: any = null
    2. Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    3. // ...
    4. const prevActiveInstance = activeInstance
    5. activeInstance = vm
    6. if (!prevVnode) {
    7. // initial render
    8. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    9. } else {
    10. // updates
    11. vm.$el = vm.__patch__(prevVnode, vnode)
    12. }
    13. activeInstance = prevActiveInstance
    14. }

    这个 activeInstance 作用就是保持当前上下文的 Vue 实例,它是在 lifecycle 模块的全局变量,定义是 export let activeInstance: any = null,并且在之前我们调用 createComponentInstanceForVnode 方法的时候从 lifecycle 模块获取,并且作为参数传入的。因为实际上 JavaScript 是一个单线程,Vue 整个初始化是一个深度遍历的过程,在实例化子组件的过程中,它需要知道当前上下文的 Vue 实例是什么,并把它作为子组件的父 Vue 实例。之前我们提到过对子组件的实例化过程先会调用 initInternalComponent(vm, options) 合并 options,把 parent 存储在 vm.$options 中,在 $mount 之前会调用 initLifecycle(vm) 方法:

    1. export function initLifecycle (vm: Component) {
    2. const options = vm.$options
    3. // locate first non-abstract parent
    4. let parent = options.parent
    5. if (parent && !options.abstract) {
    6. while (parent.$options.abstract && parent.$parent) {
    7. parent = parent.$parent
    8. }
    9. parent.$children.push(vm)
    10. }
    11. vm.$parent = parent
    12. // ...
    13. }

    可以看到 vm.$parent 就是用来保留当前 vm 的父实例,并且通过 parent.$children.push(vm) 来把当前的 vm 存储到父实例的 $children 中。

    vm._update 的过程中,把当前的 vm 赋值给 activeInstance,同时通过 const prevActiveInstance = activeInstanceprevActiveInstance 保留上一次的 activeInstance。实际上,prevActiveInstance 和当前的 vm 是一个父子关系,当一个 vm 实例完成它的所有子树的 patch 或者 update 过程后,activeInstance 会回到它的父实例,这样就完美地保证了 createComponentInstanceForVnode 整个深度遍历过程中,我们在实例化子组件的时候能传入当前子组件的父 Vue 实例,并在 _init 的过程中,通过 vm.$parent 把这个父子关系保留。

    那么回到 update,最后就是调用 _patch 渲染 VNode 了。

    1. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    2. function patch (oldVnode, vnode, hydrating, removeOnly) {
    3. // ...
    4. let isInitialPatch = false
    5. const insertedVnodeQueue = []
    6. if (isUndef(oldVnode)) {
    7. // empty mount (likely as component), create new root element
    8. isInitialPatch = true
    9. createElm(vnode, insertedVnodeQueue)
    10. } else {
    11. // ...
    12. }
    13. // ...
    14. }

    这里又回到了本节开始的过程,之前分析过负责渲染成 DOM 的函数是 createElm,注意这里我们只传了 2 个参数,所以对应的 parentElmundefined。我们再来看看它的定义:

    1. function createElm (
    2. vnode,
    3. insertedVnodeQueue,
    4. parentElm,
    5. refElm,
    6. nested,
    7. ownerArray,
    8. index
    9. ) {
    10. // ...
    11. if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    12. return
    13. }
    14. const data = vnode.data
    15. const children = vnode.children
    16. const tag = vnode.tag
    17. if (isDef(tag)) {
    18. // ...
    19. vnode.elm = vnode.ns
    20. ? nodeOps.createElementNS(vnode.ns, tag)
    21. : nodeOps.createElement(tag, vnode)
    22. setScope(vnode)
    23. /* istanbul ignore if */
    24. if (__WEEX__) {
    25. // ...
    26. } else {
    27. createChildren(vnode, children, insertedVnodeQueue)
    28. if (isDef(data)) {
    29. invokeCreateHooks(vnode, insertedVnodeQueue)
    30. }
    31. insert(parentElm, vnode.elm, refElm)
    32. }
    33. // ...
    34. } else if (isTrue(vnode.isComment)) {
    35. vnode.elm = nodeOps.createComment(vnode.text)
    36. insert(parentElm, vnode.elm, refElm)
    37. } else {
    38. vnode.elm = nodeOps.createTextNode(vnode.text)
    39. insert(parentElm, vnode.elm, refElm)
    40. }
    41. }

    注意,这里我们传入的 vnode 是组件渲染的 vnode,也就是我们之前说的 vm._vnode,如果组件的根节点是个普通元素,那么 vm._vnode 也是普通的 vnode,这里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值是 false。接下来的过程就和我们上一章一样了,先创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 createElm,在遍历的过程中,如果遇到子 VNode 是一个组件的 VNode,则重复本节开始的过程,这样通过一个递归的方式就可以完整地构建了整个组件树。

    由于我们这个时候传入的 parentElm 是空,所以对组件的插入,在 createComponent 有这么一段逻辑:

    1. function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    2. let i = vnode.data
    3. if (isDef(i)) {
    4. // ....
    5. if (isDef(i = i.hook) && isDef(i = i.init)) {
    6. i(vnode, false /* hydrating */)
    7. }
    8. // ...
    9. if (isDef(vnode.componentInstance)) {
    10. initComponent(vnode, insertedVnodeQueue)
    11. insert(parentElm, vnode.elm, refElm)
    12. if (isTrue(isReactivated)) {
    13. reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
    14. }
    15. return true
    16. }
    17. }
    18. }

    在完成组件的整个 patch 过程后,最后执行 insert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插入,如果组件 patch 过程中又创建了子组件,那么DOM 的插入顺序是先子后父。

    总结

    那么到此,一个组件的 VNode 是如何创建、初始化、渲染的过程也就介绍完毕了。在对组件化的实现有一个大概了解后,接下来我们来介绍一下这其中的一些细节。我们知道编写一个组件实际上是编写一个 JavaScript 对象,对象的描述就是各种配置,之前我们提到在 _init 的最初阶段执行的就是 merge options 的逻辑,那么下一节我们从源码角度来分析合并配置的过程。

    原文: https://ustbhuangyi.github.io/vue-analysis/components/patch.html