6. 「vue@2.6.11 源码分析」组件渲染之基于虚拟DOM的视图更新

516 阅读6分钟

这是一个系列文章,请关注 vue@2.6.11 源码分析 专栏


前情回顾

new Vue(..)之后总共有两个大的步骤,第一步是调用vm._init完成组件的各种准备(初始化)工作,然后是开始结合数据与模板实现页面的渲染。vue引入了虚拟DOM技术,这里页面渲染分为两步,将模板和数据(转为了render函数)转为虚拟DOM树,而后再将虚拟DOM树同步到界面上。上一小节已经分析过创建虚拟DOM树的过程(vm._render),现在我们来看看虚拟DOM是如何更新到界面上的,这个过程在vm._update方法上。

updateComponent = () => {
  vm._update(vm._render(), hydrating) // hydrating: ssr相关 忽略
}

new Watcher(vm, updateComponent, noop, {
  before () { /*...*/ }
}, true /* isRenderWatcher */)

vm._update

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }
 
  //...
}

入参vnode是来自上一步_render刚创建的虚拟DOM树,preVnode则是上一次创建的虚拟DOM树,prevEl是上一次组件渲染的根DOM。注意更新了vm._vnode = vnode

然后就是最关键的步骤patch,对比新老虚拟DOM树,找出差异,同步到界面上。patch方法会返回一个DOM,然后保存到$el上,同时也看到后面有更新 $el.__vue__ = vm,这样组件就和实际渲染内容的根DOM相互关联起来了。

HOC场景的 $parent.$el 更新?❎ 先遗留

下面重点看下patch方法

patch:组件diff入口

在vue当前版本中,该方法的入口总是组件去调用,因为这个方法的定义Vue.prototype上,所以只有vue实例(就是组件)可以调用。

Vue.prototype.__patch__ = inBrowser ? patch : noop

这里的核心逻辑在snabbdom流程上是类似的,参考专栏snabbdom@3.5.1 源码分析第三篇。还是看下vue@2.6.11这里的实现。

export function createPatchFunction (backend) {
    //...
    return function patch (oldVnode, vnode, hydrating, removeOnly) {
      if (isUndef(vnode)) {
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
        return
      }

      //...
      if (isUndef(oldVnode)) {
        //...createElm
      } else {
        const isRealElement = isDef(oldVnode.nodeType)
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
          //... patchVnode
        } else {
          //... createElm
        }
      }

      invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
      return vnode.elm
    }
}

总共分为四种情况,见下面分析。

  1. !vnode && oldVnode(vnode不存在但是oldVnode存在)
  2. vnode && !oldVnode(vnode存在 并且 oldVnode不存在)
  3. vnode && oldVnode(二者均存在)
    1. oldVnode不是DOM && sameVnode(oldVnode, vnode)
    2. oldVnodeDOM || !sameVnode(oldVnode, vnode)

1. !vnode && oldVnode(vnode不存在但是oldVnode存在)

直接调用invokeDestroyHook触发oldVnode销毁逻辑

invokeDestroyHook

function invokeDestroyHook (vnode) {
    let i, j
    const data = vnode.data
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
    }
    if (isDef(i = vnode.children)) {
      for (j = 0; j < vnode.children.length; ++j) {
        invokeDestroyHook(vnode.children[j])
      }
    }
  }

触发destroy相关钩子:vnode.data.hook.destroy(针对组件placeholder vnode的钩子)、cbs.destroy(cbs收集的是模块上的钩子回调)

看到vnode.data.hook.destroy的区分了keepAlive场景,普通场景下直接调用组件实例的$destroy方法(注意不是开发者提供的destroyed的生命周期方法)。至于deactivateChildComponent后面可能会单独小节分析keep-alive组件,这里暂时忽略。

const componentVNodeHooks = {
    //... init、insert、prepatch

    destroy (vnode: MountedComponentVNode) {
      const { componentInstance } = vnode
      if (!componentInstance._isDestroyed) {
        if (!vnode.data.keepAlive) {
          componentInstance.$destroy()
        } else {
          deactivateChildComponent(componentInstance, true /* direct */)
        }
      }
    }
}

下面看下vm.$destroy做了些什么

vm.$destroy()

  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
   
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
  1. 如果组件正在销毁则返回。
  2. 触发生命周期:beforeDestroy,并设置正在销毁_isBeingDestroyed标识
  3. 取消父子组件关系(parent.$children中移除当前删除的组件实例vm)
  4. watcher销毁
    1. 销毁渲染关联的watcher(在mountComponent创建的,用来渲染组件的)
    2. 销毁组件中开发者提供watch属性生成的watchers(在initState -> initWatcher中创建的)
  5. 设置已经销毁标识_isDestroyed
  6. 移除vm._vnode,同样是通过patch去处理,通过和null进行diff,来移除(和snabbdom几乎一致的思路)
  7. 触发 destroyed(开发者提供的)声明周期
  8. 取消所有的事件监听
  9. 取消关联的DOM指向js对象的引用,vm.$el.__vue__ = null
    • ❎ 否则,DOM删除不了导致内存泄漏?需要验证下 参考
  10. vm.$vnode.parent = null,keep-alive场景下的内存泄漏:issue#6759

我学习到的:需要清理一切需要清理的,并且所有的属性最好都是统一在一个地方声明,确保删除的时候没有遗漏。

小结

image.png

  • vm.$destroy:组件销毁包括DOM移除、事件和watcher等移除、触发beforeDestroydestroyed生命周期等。
  • cbs.destroy收集注册的module的对应钩子,即在调用createPatchFunction传递的modules参数(如果你看过snabbdom源码分析,就会知道他们的作用啦)。

2. vnode && !oldVnode(vnode存在 并且 oldVnode不存在)

这种情况暂时不会真正挂载界面上,因为没有提供挂载点。这种情况就两个步骤,如下:

// empty mount (likely as component), create new root element
isInitialPatch = true // 关键
createElm(vnode, insertedVnodeQueue) // 注意:没有提供挂载点:parentElm

// isInitialPatch为true时,会延迟insert hooks执行,指导真正挂载到界面上。
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) // isInitialPatch: true

首先调用createElm来创建DOM并挂载,非常重要下面会重点分析。invokeInsertHook,注意这里在调用invokeInsertHook时设置了isInitialPatch = true,逻辑如下分析。

invokeInsertHook

function invokeInsertHook (vnode, queue, initial) {
  // delay insert hooks for component root nodes, invoke them after the
  // element is really inserted
  if (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue
  } else {
    for (let i = 0; i < queue.length; ++i) {
      queue[i].data.hook.insert(queue[i])
    }
  }
}

注意isInitialPatch情况,延迟执行queue中虚拟DOM的data.hook.insert的执行,并保存到 vnode.parent.data.pendingInsert中。等真正会进行挂载的时候,采取触发。

如果不是上面场景,即会进行DOM的挂载,执行vnode.data.hook.insert。注意,执行componentVNodeHooks中的hookvnode一定是组件的placeholder vnode,因为这些hook就在创建组件placeholder vnode时安装的。

其中vnode.data.hook.insert如下:

const componentVNodeHooks = {
    insert (vnode: MountedComponentVNode) {
      const { context, componentInstance } = vnode
      if (!componentInstance._isMounted) {
        componentInstance._isMounted = true
        callHook(componentInstance, 'mounted')
      }
      //...keepAlive场景,暂且忽略
    },
    //...
}

如果组件已经没有挂载过,初次挂载,则触发mounted生命周期,并设置_isMounted标识组件已经挂载。


image.png

3. vnode && oldVnode(二者均存在)

3.1 oldVnode不是DOM && sameVnode(oldVnode, vnode)

// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) // isInitialPatch: false

两步:patchVnode -> insertInvokeHookpatchVnode很重要,后面会单独分析,其目的就是对新老两个虚拟DOM进行对比,前提是这两个虚拟DOM被判断为samveVnode,才有意义。

另外看下sameVnode的实现,如下。逻辑很简单,没什么好说的,注意针对异步加载情况单独加了判断。另外undefined === undefinedtrue,所以新老节点都没有设置的条件(如key)会略过,因为是true啊。

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

3.2 oldVnode是DOM || !sameVnode(oldVnode, vnode)

if (isRealElement) {
  // ... ssr 相关
  // create an empty node and replace it
  oldVnode = emptyNodeAt(oldVnode)
}

// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)

// create new node
// 第三个参数:issue##4590
createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm))

// update parent placeholder node element, recursively
// ...

// destroy old node
if (isDef(parentElm)) {
  removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
  invokeDestroyHook(oldVnode)
}
  1. 如果oldVnode是一个DOM,则创建一个真实的VNode,然后替换oldVnode变量(不会创建孩子的虚拟DOM)
  2. createELm,根据vnode树创建DOM树,并挂载到界面上
  3. update parent placeholder node element, recursively
// component root element replaced. // 这是之前版本的注释,解释了当前场景的背景
// update parent placeholder node element, recursively

大概知道原因了,首先这里不是组件移除,而是组件的根节点被替换的场景。看起来是组件的根节点被替换的时候需要做一些特殊处理,先cbs.destroy,再cbs.create,显然和模块有关。猜测模块测createdestroy钩子主要用来处理组件placeholder vnode(源码注释中的placeholder node element)和组件根vnode(源码注释中的component root element)关系的。先遗留❎,后续分析模块时,再验证。

另外这里还涉及两个issue:issue#6718issue#6513。看起来是这种场景特殊case的处理。

另外再看下 isPatchable方法的含义,fix patch modules error on empty component root 首先这里的vnode是组件的根vnode,isPatchable用来判断递归根孩子组件中最后一个根孙子组件的根节点的tag是否存在。假设当前组件是component-a,则isPatchable()会返回false。所以这个函数的传达的含义是这个vnode是否可以patch?。

<!-- component-a组件的template -->
<component-b></component-b> <!-- 该组件的根节点是一个组件-->

<!-- component-b 组件的template -->
</component-c> <!-- 该组件的根节点是一个组件 -->

<!-- component-c 组件的template -->
<component-c>
    <> || 文本节点 <!-- 空节点、文本节点 -->
</component-c>
  1. 移除节点:如果oldVnode.elm存在,则调用removeVnodes删除oldVnode.elm;否则(说明是oldVnode是组件placeholder vnode),针对组件则调用invokeDestroyHook来处理。

下面单独说一下removeVnodes

removeVnodes -> xxxInvokeRemoveHook

function removeVnodes (vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    const ch = vnodes[startIdx]
    if (isDef(ch)) {
      if (isDef(ch.tag)) {
        removeAndInvokeRemoveHook(ch)
        invokeDestroyHook(ch)
      } else { // Text node
        removeNode(ch.elm)
      }
    }
  }
}

如果是文本节点,直接removeNode(dom操作,parent.removeChild(...)

否则如果tag存在,则可能是浏览器内置标签如div,也可能是组件(如todo-item))

  • removeAndInvokeRemoveHook 用来触发remove相关的钩子、递归处理子组件、删除当前元素rm()
  • invokeDestroyHook 触发destroy相关的的钩子

removeAndInvokeRemoveHook区分了普通节点和组件节点。

如果是普通节点如div则直接removeNode移除就好。

如果是组件的placeholder vnode则需要触发组件的remove相关的钩子,并且递归删除组件实际渲染内容的根节点即在vm._render()返回的虚拟DOM,保存在vm._vnode上。

function removeAndInvokeRemoveHook (vnode, rm) {
  if (isDef(rm) || isDef(vnode.data)) {
    let i
    const listeners = cbs.remove.length + 1
    if (isDef(rm)) {
      // we have a recursively passed down rm callback
      // increase the listeners count
      rm.listeners += listeners
    } else {
      // directly removing
      rm = createRmCb(vnode.elm, listeners)
    }
    // recursively invoke hooks on child component root node
    if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
      removeAndInvokeRemoveHook(i, rm)
    }
    for (i = 0; i < cbs.remove.length; ++i) {
      cbs.remove[i](vnode, rm)
    }
    if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
      i(vnode, rm)
    } else {
      rm()
    }
  } else {
    removeNode(vnode.elm)
  }
}

rm()是最终删除DOM的方法,注意到该方法有一个listeners属性,用来保存有多个cbs.removecbs.remove收集自模块,说明模块监听了该事件,此时会将rm的调用交给模块,只有当所有的模块执行完成以后,才能真正删除DOM,下面下面remove方法中if条件.

function createRmCb (childElm, listeners) {
  function remove () {
    if (--remove.listeners === 0) {
      removeNode(childElm)
    }
  }
  remove.listeners = listeners
  return remove
}

小结

image.png

根据vnode创建DOM

createElm:根据vnode创建DOM并挂载到树中

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) { 

    vnode.elm = nodeOps.createElement(tag, vnode)
    
    setScope(vnode)

    createChildren(vnode, children, insertedVnodeQueue)
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
    }
    insert(parentElm, vnode.elm, refElm)

  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

首先:第一个ifcloneNode逻辑,是有特殊场景的,见commit # 956756b1提交里面有test case,这里暂不深究 ❎。

然后(关键):根据placeholder vnode尝试创建子组件实例并渲染子组件,createComponent返回true,说明当前vnode关联的是一个组件,否则进入后面逻辑(非组件情况)。


创建元素并挂载,区分三种情况:tag存在、isCommenttext vnode,下面重点看下最常见的场景tag存在时

  • 首先是通过document.createElement创建该元素
  • setScope是为了支持 scoped CSS. 特性的。 暂遗留 ❎
  • 调用createChildren递归创建孩子
    function createChildren (vnode, children, insertedVnodeQueue) {
      if (Array.isArray(children)) {
        for (let i = 0; i < children.length; ++i) {
          createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
        }
      } else if (isPrimitive(vnode.text)) {
        nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
      }
    }
    

触发create相关的钩子,并保存新创建到元素到insertVnodeQueue,后面invokeInsertHooks时会用到。cbs.create是数组是因为会存在多个模块需要处理该元素(主体是模块),而vnode.data.hook.create只是用来处理自身的(主体是自己)

function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}

最后,将创建的DOM挂载或者衔接(不一定是挂载到界面上)

小结

image.png


补充: isComment = true的情况,部分场景可能会调用创建一个空节点,vue中通过注释节点模拟实现。

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

createComponent:根据placeholder vnode创建(子)组件实例并渲染

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    // after calling the init hook, if the vnode is a child component
    // it should've created a child instance and mounted it. the child
    // component also has set the placeholder vnode's elm.
    // in that case we can just return the element and be done.
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive) {
        // keepAlive 场景,暂忽略
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  //... insert、prepatch、destroy
}

首先会调用vnode.data.hook.initcomponentVNodeHooks.init,不考虑keepAlive场景下,这里会调用createComponentInstanceForVnode创建子组件实例,而后调用$mount进行子组件的渲染。和我们之前的文章new Vue() 整体流程对应上了是不是,整个过程两个大的步骤:实例初始化 + 渲染


上面创建完组件实例后,会将组件实例保存到vnode.componentInstance上,如果存在组件实例后则会调用initComponent,而后调用insert方法将组件内容挂载到dom树上。

下面分析下initComponent


function initComponent (vnode, insertedVnodeQueue) {
  if (isDef(vnode.data.pendingInsert)) {
    insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
    vnode.data.pendingInsert = null
  }
  vnode.elm = vnode.componentInstance.$el
  if (isPatchable(vnode)) {
    invokeCreateHooks(vnode, insertedVnodeQueue)
    setScope(vnode)
  } else {
    // empty component root.
    // skip all element-related modules except for ref (#3455)
    registerRef(vnode)
    // make sure to invoke the insert hook
    insertedVnodeQueue.push(vnode)
  }
}

上面部分介绍patch的第二个场景vnode && !oldVnode会创建一个游离组件的场景的处理,当这个遗留组件被利用时(需要挂载到dom树中时),则需要暂遗留将之前存储在vnode.data.pendingInsert中的insertedVnodeQueue取出,因此这个阶段需要被应用了。 这种游离组件,具体是什么场景,暂遗留 ❎。

然后就是,将根DOM保存到placeholder vnode.elm上。而后就是触发create相关的钩子,当前版本并未使用vnode.data.hook.create钩子,然后调用setScope,scoped css相关,暂遗留。

至于else里面的分支是处理empty component root这种特殊场景的,如:fix patch modules error on empty component rootissue#3455:fix ref on empty component root 。看来这个empty component root会引起很多问题啊,isPatchable方法就是用来判断是不是empty component root,如果是则不能patchisPatchable返回false

小结

image.png

patchVnode & updateChildren

patch.js

这两个方法和snabbdom中的实现几乎完全一致,可以参考,下面重点说下patchVnode差异部分。

如果是异步组件,则走异步组件加载的处理,return

如果是静态节点,则走静态节点的优化处理:isStatic,编译环节会给静态节点添加该标记。参考官方解释-static-hoisting,目的是对于此类节点在更新时没必要重新构造vnode然后对比。 return

然后如果是组件placeholder vnode,则通过调用vnode.data.hook.prepatch实现组件placeholder vnode的更新。看到在prepatch方法中调用了updateChildComponent来进行组件数据(属性,事件等)的更新

// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
    prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
      const options = vnode.componentOptions
      const child = vnode.componentInstance = oldVnode.componentInstance
      updateChildComponent(
        child,
        options.propsData, // updated props
        options.listeners, // updated listeners
        vnode, // new parent vnode
        options.children // new children
      )
    },
}

然后触发update相关的钩子(cbs.update、vnode.data.hook.update)

最后触发vnode.data.hook.postpatch。搜索src/发现可能和directives有关,后面会单独章节分析指令相关,暂遗留 ❎

这里主要区别是针对组件vnode的处理:updateChildComponent

updateChildComponent

在之前创建的组件实例中,组件vue实例是保存在vnode.componentInstance中,但是由于属性值、事件等都可能发生了变化,因此需要更新。虽然组件实例不会重新创建,但是组件标签本身关联的place holder vnode还是会重新创建(新的vnode),并且在_render -> componentComponent会获取最新的componentOptions,保存到vnode.componentOptions。 因此这里就是将新的vnode.componentOptions更新到oldVnode.componentInstance中。

注意:由于这里给vm._props重新赋值了,因此组件中computedwatch渲染watcher等订阅的观察者都会被触发。

export function updateChildComponent (
  vm: Component, propsData: ?Object, listeners: ?Object,
  parentVnode: MountedComponentVNode, renderChildren: ?Array<VNode>) {
 
  //... slot 相关,暂且忽略,后面可能小节分析

  vm.$options._parentVnode = parentVnode
  vm.$vnode = parentVnode // update vm's placeholder node without re-render

  if (vm._vnode) { // update child tree's parent
    vm._vnode.parent = parentVnode
  }
  vm.$options._renderChildren = renderChildren

  // update $attrs and $listeners hash
  // these are also reactive so they may trigger child update if the child
  // used them during render
  vm.$attrs = parentVnode.data.attrs || emptyObject
  vm.$listeners = listeners || emptyObject

  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }

  // update listeners
  listeners = listeners || emptyObject
  const oldListeners = vm.$options._parentListeners
  vm.$options._parentListeners = listeners
  updateComponentListeners(vm, listeners, oldListeners)

  //... slot相关 暂且忽略

}

小结

patchVnode的主要作用是调用updateChildren来实现节点的复用,另外关键的一点是对于组件的情况调用updateChildComponent来更新子组件的信息(属性值的重新赋值,事件的重新绑定等),如果子组件的属性发生变更,则会引起子组件的重新渲染。

另外注意的是patchVnode是针对samveVnode场景的,即oldVnode一定可以被复用的情况,因此当然可以在patchVnode方法中调用updateChildComponent是合理的,因为此时组件一定会被复用。

总结

组件渲染的整体流程如下:

vue-2.6.11.drawio.png