logo
blog
readme
Back to Blog
Back to Blog
Back to Blog

‌

‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌

© 2025 linzhe. All rights reserved.

TODO:编辑

vue3的diff流程

图片笔记

重点newIndexToOldIndexMap(下标为新节点索引,值为旧节点索引+1)

const old = (a) b c d (e)
const new = (a) c d b (e)

demo

调用栈大概如下patchElement('divWrapper')->patchChildren->patchKeyedChildren,重点就是patchKeyedChildren

首先会两个children数组的头部方向开始比较

ts
// 第一个while循环
while (i <= e1 && i <= e2)

如果的元素是相同的(key和type都相同),则只会patchElement(c1,c2),并且继续对比下一个,如果遇到不是相同的元素,就break,这时也知道了对比了多少个相同的元素了。

然后会两个children数组的尾部方向开始比较

ts
// 第二个while循环
while (i <= e1 && i <= e2)

如果的元素是相同的(key和type都相同),则只会patchElement(c1,c2),并且继续对比前一个,如果遇到不是相同的元素,就break,这时也知道了对比了多少个相同的元素了。

Image

字母是key,数字是所在children数组的下标

求最长递增子序列

接下来发现在 old 中还剩b:1 c:2 d:3, new 中还剩c:1 d:2 b:3,这也是diff的关键所在,首先我们记录在新的children中,各个key对应的下标,既keyToNewIndexMap。

Image

然后要处理剩余的old,在新的剩余项中keyToNewIndexMap,如果有对应的key,就patch(prevChild, c2),否则就unmount(prevChild),如果存在vue不仅要patch,还要处理位置是否移动,vue通过最长子序列来实现。所以我们需要知道在新数组中的元素他们原来在旧数组中所在的位置。

js
// 为什么 newIndex - s2 这表示在剩余新增元素组成数组中的位置(下标)
// 这个+1 是一个特殊处理,因为vue 内部0 表示这个元素需要挂载

newIndexToOldIndexMap[newIndex - s2] = i + 1
// 这里就算是加10也无所谓,重点是趋势
// 我在fork的仓库中尝试了+10,单元测试是完全通过的

Image

旧元素剩余数组b:1 c:2 d:3

  • 第一个元素b:1,在新剩余数组中的下标是2= 3(新数组中的下标)- s2,newIndexToOldIndexMap中的b元素的值就是2=1(旧完整数组所在的位置,也就是冒号后面的数字) + 1
  • 第二个元素c:2,在新剩余数组中的下标是0=1(新数组中的下标)- s2,newIndexToOldIndexMap中的c元素的值就是3=2(旧完整数组所在的位置,也就是冒号后面的数字) + 1
  • 第二个元素d:3,在新剩余数组中的下标是1=2(新数组中的下标)- s2,newIndexToOldIndexMap中的d元素的值就是4=3(旧完整数组所在的位置,也就是冒号后面的数字) + 1

Image

得到最新递增子序列[3,4]也就是c,d,那么就表示c,d不需要移动。 最后在遍历新的剩余元素组成的数组,如果元素不在最长递增子序列中,并且又不是新增元素,那么就移动位置。

源码

ts
const patchKeyedChildren = (
  c1: VNode[],
  c2: VNodeArrayChildren,
  container: RendererElement,
  parentAnchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  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,
        namespace,
        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,
        namespace,
        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,
          namespace,
          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<PropertyKey, 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 {
        // 建立旧child和新child的位置映射
        // newIndexToOldIndexMap[下标] = 旧数组中的位置
        // 下标就是0,1,2等就是新数组的 child
        // 比如 [0:c,1:d,2:b]
        newIndexToOldIndexMap[newIndex - s2] = i + 10
        if (newIndex >= maxNewIndexSoFar) {
          maxNewIndexSoFar = newIndex
        } else {
          moved = true
        }
        patch(
          prevChild,
          c2[newIndex] as VNode,
          container,
          null,
          parentComponent,
          parentSuspense,
          namespace,
          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,
          namespace,
          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]) {
          // 如果new child 不在 最长递增子序列中,就移动
          move(nextChild, container, anchor, MoveType.REORDER)
        } else {
          j--
        }
      }
    }
  }
}