vue2和vue3的diff算法

254 阅读10分钟

本文主要分析vue2和vue3 diff算法的不同。根据网上文章以及源码来加深对算法的理解,在此做个记录,方便日后查阅。

vue2 diff算法

参考链接:说说 vue2 和 vue3 核心diff算法 在这篇文章中,对vue2的diff算法讲解得比较清楚,可根据dom节点的变化过程加深对代码的理解。 但是在新前与旧后小节中,更新后的节点不对,应为:afcdebg

vue2 核心代码

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 旧子节点的开始索引
    let oldStartIdx = 0 
    // 新子节点的开始索引
    let newStartIdx = 0
    // 旧子节点的结束索引
    let oldEndIdx = oldCh.length - 1
    // 第一个旧节点
    let oldStartVnode = oldCh[0]
    // 最后一个旧节点
    let oldEndVnode = oldCh[oldEndIdx]
    // 新子节点的结束索引
    let newEndIdx = newCh.length - 1
    // 第一个新节点
    let newStartVnode = newCh[0]
    // 最后一个新节点
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 因为暴力对比过程把移动的vnode置为 undefined 如果不存在节点直接跳过
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
        // 新开始节点和旧的开始节点相同
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 递归比较儿子以及他们的子节点
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
        // 新尾节点和旧的尾节点相同
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 递归比较儿子以及他们的子节点
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
        // 旧头节点与新尾节点相同
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
        // 旧尾节点与新头节点相同
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 用新的开始节点的key,去老的子节点生成的映射表中查找
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // 原来的位置用undefined占位 避免数组塌陷 防止老节点移动走了之后破坏了初始的映射表位置
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // 如果老节点循环完毕了 但是新节点还有,需要把剩余的节点插入
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) { // 如果新节点循环完毕了 但是老节点还有,需要把剩余的节点删除
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }
  
  function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

vue2 diff整体流程图

vue3 diff算法

vue2的diff算法在本文的参考链接中讲解得很好,通俗易懂,但是vue3的算法讲解中定义的一些变量似乎跟源码有些出入,因此在这里再重新写一遍。

vue3的diff算法的源码在packages/runtime-core/src/renderer.ts文件中,核心方法是patchKeyedChildren。处理步骤为:

  1. 前序对比算法
  2. 尾序对比算法
  3. 新节点如果多出来就是挂载
  4. 旧节点如果多出来就是删除
  5. 特殊情况乱序,根据新旧节点的索引映射关系构建最长递增子序列。最长递增子序列所指向的节点即为不需要移动的节点。

以下以初始节点abcdefg最后更新为abfcdehg为例来说明整个更新过程。

未命名2.png

相同前置节点对比

image.pngimage.png (图中的新旧节点的标注反了)

前置节点的处理是定义了一个i变量,分别指向新,老两个组子节点,比较指向的新、老节点是否相同,如果相同指针 +1,直到两个节点不同时结束前置节点的处理。

相同后置节点对比

image.png (图中的新旧节点的标注反了)

后置节点的处理是定义了两个变量,分别为e1和e2,e1指向旧的一组子节点的最后一个索引,e2指向新的一组子节点的最后一个索引。然后比较两个索引指向的新旧节点,如果指向相同,则这两个索引都 -1,直到两个节点不同时结束后置节点的处理。

剩余节点的处理

处理完相同的前置节点和后置节点之后,如果还有剩余节点,需处理剩余节点,剩余节点的情况分为三种,分别为:

  1. 老节点已经遍历完,新节点还有剩余
  2. 新节点已遍历完,老节点还有剩余
  3. 新老节点都有剩余

新节点还有剩余

image.png 当满足条件i > e1 并且 i <= e2时,说明旧节点已遍历完,新节点还有剩余,则要循环i -> e2的节点进行循环插入。

老节点有剩余

image.png 当满足条件i > e2并且i <= e1时,说明新节点已遍历完,旧节点还有剩余,此时需要循环i ->e1的节点进行卸载。

新老节点均有剩余

经过前序对比和后序对比之后(旧节点:abcdefg 新节点:abfcdehg),i以及e1e2的值分别为:

image.png 代码的核心流程为:

  1. 定义s1(旧节点循环开始索引)和s2(新节点循环的开始索引),初始值均为i。建立新节点的keyindex的映射关系,存在变量keyToNewIndexMap(map类型)中,值为:
f => 2,
c => 3,
d => 4,
e => 5,
h => 6
  1. 循环遍历需要patch的旧节点,patch能与新节点匹配的旧节点同时移除已经不存在的节点,具体过程为:

    • 定义newIndexToOldIndexMap(数组)变量,存储新节点的索引与旧节点索引的映射关系,长度为需要打补丁的节点个数即为e2-s2+1,每个元素的初始值为0,newIndexToOldIndexMap=[0, 0, 0, 0, 0]
    • 循环遍历剩余的需要patch的旧字节点。在每次遍历中,通过旧节点的key获取新节点的索引值newIndex,如果newIndexundefined,则说明该节点不在新节点数组中,则删除该节点,如果有值,则更新newIndexToOldIndexMap的值,每一项的计算方式为:newIndexToOldIndexMap[newIndex-s2]=i+1。更新后值为:[6, 3, 4, 5, 0]

补充说明:在该步骤中,会定义一个变量moved,用于判断节点是否会出现交叉,该变量会用在步骤3中。变量的更新方式为:定义一个辅助变量maxNewIndexSoFar(初始值为0),如果newIndex >= maxNewIndexSoFar,则maxNewIndexSoFar=newIndex,否则moved=true

image.png

  1. 获取最长递增子序列,根据不同情况添加、移动或者什么都不做

    • 根据步骤2知,movedtrue,获取最长递增子序列的索引保存在increasingNewIndexSequence中,值为[1, 2, 3]
    • 从后往前遍历需要patch的新节点,如果newIndexToOldIndexMap[i] === 0,说明为新增节点,否则如果为需要移动的节点,则根据increasingNewIndexSequence[j]的值与i的值,是否相同,来确定是否要移动节点,或者什么都不做。循环的具体流程如下:

第一次循环: image.png

第二次循环: image.png 经过四次循环后:

image.png 第五次循环:

image.png

vue3 diff算法核心代码

// can be all-keyed or mixed
  const patchKeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    parentAnchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    let i = 0
    const l2 = c2.length
    let e1 = c1.length - 1 // prev ending index
    let e2 = l2 - 1 // next ending index

    // 1. sync from start
    // (a b) c
    // (a b) d e
    while (i <= e1 && i <= e2) {
      const n1 = c1[i]
      const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        break
      }
      i++
    }

    // 2. sync from end
    // a (b c)
    // d e (b c)
    while (i <= e1 && i <= e2) {
      const n1 = c1[e1]
      const n2 = (c2[e2] = optimized
        ? cloneIfMounted(c2[e2] as VNode)
        : normalizeVNode(c2[e2]))
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        break
      }
      e1--
      e2--
    }

    // 3. common sequence + mount
    // (a b)
    // (a b) c
    // i = 2, e1 = 1, e2 = 2
    // (a b)
    // c (a b)
    // i = 0, e1 = -1, e2 = 0
    if (i > e1) {
      if (i <= e2) {
        const nextPos = e2 + 1
        const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
        while (i <= e2) {
          patch(
            null,
            (c2[i] = optimized
              ? cloneIfMounted(c2[i] as VNode)
              : normalizeVNode(c2[i])),
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          i++
        }
      }
    }

    // 4. common sequence + unmount
    // (a b) c
    // (a b)
    // i = 2, e1 = 2, e2 = 1
    // a (b c)
    // (b c)
    // i = 0, e1 = 0, e2 = -1
    else if (i > e2) {
      while (i <= e1) {
        unmount(c1[i], parentComponent, parentSuspense, true)
        i++
      }
    }

    // 5. unknown sequence
    // [i ... e1 + 1]: a b [c d e] f g
    // [i ... e2 + 1]: a b [e d c h] f g
    // i = 2, e1 = 4, e2 = 5
    else {
      const s1 = i // prev starting index
      const s2 = i // next starting index

      // 5.1 build key:index map for newChildren
      const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
      for (i = s2; i <= e2; i++) {
        const nextChild = (c2[i] = optimized
          ? cloneIfMounted(c2[i] as VNode)
          : normalizeVNode(c2[i]))
        if (nextChild.key != null) {
          if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
            warn(
              `Duplicate keys found during update:`,
              JSON.stringify(nextChild.key),
              `Make sure keys are unique.`
            )
          }
          keyToNewIndexMap.set(nextChild.key, i)
        }
      }

      // 5.2 loop through old children left to be patched and try to patch
      // matching nodes & remove nodes that are no longer present
      let j
      let patched = 0
      const toBePatched = e2 - s2 + 1
      let moved = false
      // used to track whether any node has moved
      let maxNewIndexSoFar = 0
      // works as Map<newIndex, oldIndex>
      // Note that oldIndex is offset by +1
      // and oldIndex = 0 is a special value indicating the new node has
      // no corresponding old node.
      // used for determining longest stable subsequence
      const newIndexToOldIndexMap = new Array(toBePatched)
      for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0

      // 循环遍历旧节点
      for (i = s1; i <= e1; i++) {
        const prevChild = c1[i]
        if (patched >= toBePatched) { // 说明所有的新节点都已经遍历完了
          // all new children have been patched so this can only be a removal
          unmount(prevChild, parentComponent, parentSuspense, true)
          continue
        }
        let newIndex
        if (prevChild.key != null) {
          newIndex = keyToNewIndexMap.get(prevChild.key)
        } else {
          // key-less node, try to locate a key-less node of the same type
          for (j = s2; j <= e2; j++) {
            if (
              newIndexToOldIndexMap[j - s2] === 0 &&
              isSameVNodeType(prevChild, c2[j] as VNode)
            ) {
              newIndex = j
              break
            }
          }
        }
        if (newIndex === undefined) { // 如果新节点不包含旧节点,则给它删了
          unmount(prevChild, parentComponent, parentSuspense, true)
        } else {
          newIndexToOldIndexMap[newIndex - s2] = i + 1  // newIndexToOldIndexMap,未被遍历的新节点的数组,值初始化为0
          if (newIndex >= maxNewIndexSoFar) {
            maxNewIndexSoFar = newIndex
          } else { // 节点出现交叉,说明是移动需要去求最长递增子序列
            moved = true
          }
          patch(
            prevChild,
            c2[newIndex] as VNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          patched++
        }
      }

      // 5.3 move and mount
      // generate longest stable subsequence only when nodes have moved
      const increasingNewIndexSequence = moved
        ? getSequence(newIndexToOldIndexMap)
        : EMPTY_ARR
      j = increasingNewIndexSequence.length - 1
      // looping backwards so that we can use last patched node as anchor
      for (i = toBePatched - 1; i >= 0; i--) {
        const nextIndex = s2 + i
        const nextChild = c2[nextIndex] as VNode
        const anchor =
          nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
        if (newIndexToOldIndexMap[i] === 0) {
          // mount new
          patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (moved) {
          // move if:
          // There is no stable subsequence (e.g. a reverse)
          // OR current node is not among the stable sequence
          if (j < 0 || i !== increasingNewIndexSequence[j]) {
            move(nextChild, container, anchor, MoveType.REORDER)
          } else {
            j--
          }
        }
      }
    }
  }

vue3算法流程图

image.png

image.png

vue3_diff_unknown_sequence (2).png

vue2与vue3 diff算法的区别

vue2、vue3 的 diff 算法实现差异主要体现在:处理完首尾节点后,对剩余节点的处理方式。

在 vue2 中是通过对旧节点列表建立一个 { key, oldVnode }的映射表,然后遍历新节点列表的剩余节点,根据newVnode.key在旧映射表中寻找可复用的节点,然后打补丁并且移动到正确的位置。

而在 vue3 中是建立一个存储新节点数组中的剩余节点在旧节点数组上的索引的映射关系数组,建立完成这个数组后也即找到了可复用的节点,然后通过这个数组计算得到最长递增子序列,这个序列中的节点保持不动,然后将新节点数组中的剩余节点移动到正确的位置。