0%

Vnode节点patch过程分析

在分析前首先提出几个问题:

  • 什么时候、在哪里进行 Vnode 节点的 patch ?
  • patch 过程主要做了什么?
  • Vnode 节点如何进行新旧节点的比对、更新?

基于 Vue.js v2.6.10 进行分析

patch 方法什么时候、在什么地方调用?

组件挂载和更新时会调用 updateComponent 方法,updateComponent 中会调用组件实例上的 _update 方法,_update 方法中会对组件实例上的 __patch__ 方法进行调用。_update 的第一个参数为调用组件实例 render 方法生成的新的 Vnode 节点,在 _update 方法内,可以通过组件实例获取当前的(旧) Vnode 节点, 如果旧 Vnode 节点不存在,则说明为初始渲染,那么 __patch__ 方法的第一个参数就是组件实例 $el 属性绑定的元素,否则为旧的 Vnode 节点,__patch__ 的第二个参数为新的 Vnode 节点。而 __patch__ 方法是 patch 方法的别名。

patch 方法主要做了什么?

patch 方法通过对 vnode oldVnode 进行判断来执行不同的操作:

  • oldVnode 不存在, 根据 vnode 创建 dom 元素
  • oldVnodedom 元素或与 vnode 不为 sameVnode,则销毁 oldVnode,根据 vnode 生成 dom 元素
  • oldVnode vnodesameVnodeoldVnode 不为 dom 元素, 则执行 patchVnode 方法

patch 方法源码分析如下:

  1. 首选判断 vnode 节点是否存在,如果不存在 vnode 节点 ,说明需要销毁当前 oldVnode 节点,然后退出 patch 过程
    1
    2
    3
    4
    if (isUndef(vnode)) {
    if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
    return
    }
  2. 然后判断 oldVnode 是否存在, 如果 oldVnode 不存在,说明是初始 patch,则根据 vnode 生成相应的元素,最后返回生成的元素退出 patch 过程
    1
    2
    3
    4
    5
    6
    7
    8
    if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  3. 如果 oldVnode 存在, 在 oldVnode 不为 dom 元素且与 sameVnode(oldVnode, vnode) 返回真的情况下,执行 patchVnode 方法
    1
    2
    3
    4
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
    // patch existing root node
    patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    }
  4. 如果第三步为假,则根据 vnode 生成相应的元素,并删除 oldVnode,然后退出 patch 过程

什么情况下两个 vnode 节点是 sameVnode ?

基本要求:两个节点的 key 相同,如果两个 vnode 节点的 key 都为 undefined,则条件也成立

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
)
)
)

Vnode 是如何进行新老节点比对、更新的?

  • 通过 patchVnode 进行新,旧节点比对、更新
  • 通过 updateChildren 进行新旧节点的子节点的比对、更新

patchVnode 方法中主要做了如下操作:

  • 如果 oldVnode === vnode, 节点没有变化,直接返回
    1
    2
    3
    if (oldVnode === vnode) {
    return
    }
  • 如果是 oldVnode vnode 都为静态节点、key 相同、vnodeisCloned 为真或 vnode.isOnce 为真,则 vnode 直接从 oldVnode 中获取 componentInstance, 然后返回
    1
    2
    3
    4
    5
    6
    7
    8
    if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
    }
  • 如果 vnode.text 未定义:
    • oldVnode.childrenvnode.children 存在且不相同,则调用 updateChildren 方法进行子节点的比对
      1
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    • vnode.children 存在,执行添加子节点操作
      1
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    • oldVnode.children 存在,执行删除子节点操作
      1
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  • 如果 oldVnode.textvnode.text 不相同,则更新为 vnode.text
    1
    2
    3
    if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
    }

updateChildren 方法内做了如下操作:

首先定义四个索引变量标记新旧子节点数组(choldCh)的首尾,后面使用这些索引在子节点数组中移动进行子节点的比对

  • oldStartIdx newStartIdx 分别标记 choldCh 的首节点
  • oldEndIdx newEndIdx 分别标记 choldCh 的尾节点

然后在一个循环中根据不同的情况对索引进行移动,当新子节点或旧子节点的首尾索引相遇时说明子节点已被比对完毕,此时应中止循环

1
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)

循环中止后,有如下情况需要判断:

  • 如果是 oldStartIdx > oldEndIdx 导致循环中止,说明 newStartIdxnewEndIdx 之间的节点是新添加的,所以需要把它们添加到父节点中
  • 如果是 newStartIdx > newEndIdx 导致循环中止,说明 oldStartIdxoldEndIdx 之间的节点被删除,所以需要从父节点中删除它们

接下来就是 updateChildren 进行子节点比对的核心逻辑了:

vnode patch

  • newStartNodeoldStartNode 比对

    • 如果为 sameVnode,则调用 patchVnode 方法,最后 oldStartIdx newStartIdx 向右移动
  • newEndNodeoldEndNode 比对

    • 如果为 sameVnode,则调用 patchVnode 方法,最后 oldEndIdx newEndIdx 向左移动
  • newEndNodeoldStartNode 比对

    • 如果为 sameVnode,则调用 patchVnode 方法,然后把 oldStartIdx 标识的节点移动到 oldEndIdx 标识的节点的后面,最后 oldStartIdx 向右移动,newEndIdx 向左移动
  • newStartNodeoldEndNode 比对

    • 如果为 sameVnode,则调用 patchVnode 方法,然后把 oldEndIdx 标识的节点移动到 oldStartIdx 标识的节点的前面,最后 newStartIdx 向右移动,oldEndIdx 向左移动
  • 上面的条件都不成立

  • 创建 oldCh 中子节点的 key 与 子节点的在 oldCh 中的索引的映射,查询 newStartIdx 指向的 vnode 节点的 key 是否与映射中的 key 存在相同的情况,如果不存在则根据 newStartIdx 指向的 vnode 创建 dom 元素并插入到 oldStartIdx 指向的 vnode 节点的 dom 元素的前面 ; 如果存在且与映射中找到的节点为 sameVnode, 则把找到的节点移动到 oldStartIdx 指向的节点的前面,并把找到的节点在映射中置为 undefined,防止后面重复匹配,否则根据 newStartIdx 指向的 vnode 创建 dom 元素并插入到 oldStartIdx 指向的 vnode 节点的 dom 元素的前面