Vue3源码——diff

160 阅读5分钟

1 背景

了解框架的部分核心功能的实现是有必要的,不仅可以加深对框架的理解,在应用方面更加得心应手。其中包含了一些思想也可以在平时的学习工作中用到。

2 diff算法源码分析

vue3的diff算法的核心代码在runtime-core下面的render.ts文件中。这里使用了2个函数来处理不同的情况:patchKeyedChildren处理有key的情况,一般来说v-for中设置了唯一key的节点就会执行这个逻辑;patchUnkeyedChildren处理没有key的情况。

2.1 参数

简单讲一下参数

  • c1: VNode[]:旧的 VNode 数组。
  • c2: VNodeArrayChildren:新的 VNode 数组。
  • container: RendererElement:要将 VNode 渲染到的容器元素。
  • anchor: RendererNode | nullanchor 参数可以是一个节点对象,表示新元素将插入到该节点之前。如果 anchor 参数为 null,则表示新元素将直接插入到容器的最后。
  • parentComponent: ComponentInternalInstance | null:父组件的内部实例,可以为 null。
  • parentSuspense: SuspenseBoundary | null:父级 Suspense 边界,可以为 null。
  • isSVG: boolean:一个布尔值,表示是否渲染为 SVG 元素。
  • slotScopeIds: string[] | null:作用域插槽的 ID 数组,可以为 null。
  • optimized: boolean:是否启用了优化模式。为 true 时,函数会对新的 VNode 进行克隆操作,为 false 时,会修改原来的节点。
if (patchFlag > 0) {
  if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
    // this could be either fully-keyed or mixed (some keyed some not)
    // presence of patchFlag means children are guaranteed to be arrays
    patchKeyedChildren(
      c1 as VNode[],
      c2 as VNodeArrayChildren,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
    return
  } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
    // unkeyed
    patchUnkeyedChildren(
      c1 as VNode[],
      c2 as VNodeArrayChildren,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
    return
  }
}

3 没有key的情况:patchUnkeyedChildren

image.png

可以参考这张图的流程,比较简单,就是在共同长度对比,如果一样就复用。然后在超出部分的话,就删除或者增加

const patchUnkeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    c1 = c1 || EMPTY_ARR
    c2 = c2 || EMPTY_ARR
    const oldLength = c1.length
    const newLength = c2.length
    const commonLength = Math.min(oldLength, newLength)
    let i
    for (i = 0; i < commonLength; i++) {
      const nextChild = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      patch(
        c1[i],
        nextChild,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
    if (oldLength > newLength) {
      // remove old
      unmountChildren(
        c1,
        parentComponent,
        parentSuspense,
        true,
        false,
        commonLength
      )
    } else {
      // mount new
      mountChildren(
        c2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized,
        commonLength
      )
    }
}

4 存在key的情况patchKeyedChildren

image.png 大致的处理流程可以看这幅图,前面的四个步骤比较容易理解: 在步骤1、2中比较新旧节点数组的头部、尾部;如果其中有一个数组被遍历完了,就执行步骤3、4。

比较难理解的是步骤5.1-5.3的代码。

  • 在5.1中根据新的节点数组c2key和节点位置构建了keyToNewIndexMap,在后面会用来做新旧节点的匹配;
  • 在5.2中创建数组newIndexToOldIndexMap,用于记录哪些新节点可以复用旧节点;
  • 在5.2用moved变量判断可复用的旧节点的顺序是否会变化;
  • 在5.2中卸载未被使用到的旧节点;
  • 在5.3中,如果moved为真,也就是节点顺序有变化,就先生成最长稳定子序列,这个序列中的结点代表可以原地复用
  • 在5.3中,根据newIndexToOldIndexMap在对应的位置挂载新的节点
  • 在5.3中,根据最长稳定子序列,使用move函数来移动乱序的节点
// 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
  // patched是计数器
  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
  // 创建数组,长度是新节点数组的剩余长度,toBePatched,初始数字0,0代表是新增节点,1代表可复用旧节点
  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
    }
    // 获取newIndex
    let newIndex
    // 如果旧节点存在key
    if (prevChild.key != null) {
      newIndex = keyToNewIndexMap.get(prevChild.key)
    // 不存在key的话,就一个个遍历
    } else {
      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 {
      // 数组对应项置1,代表c2数组中的对应节点已经找到了可以复用的旧节点
      newIndexToOldIndexMap[newIndex - s2] = i + 1
      // 更新 maxNewIndexSoFar,如果有旧数组中能复用的节点是乱序的moved 就设置为 true
      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
  // 如果需要moved,就需要生成最长稳定子序列
  const increasingNewIndexSequence = moved
    ? getSequence(newIndexToOldIndexMap)
    : EMPTY_ARR
  j = increasingNewIndexSequence.length - 1
  // looping backwards so that we can use last patched node as anchor
  // toBePatched是
  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
      )
      // 如果不是新的节点并且被移动过了,就需要move
    } 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--
      }
    }
  }
}

步骤1-4的代码如下所示

  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 更多

在vue3中,diff算法的源码相比于vue2又进行了优化。

vue2是一个双端对比的diff算法,也就是还会对比旧节点数组的头和新节点数组的尾、旧节点数组的尾和新节点数组的头。

在后面节点的处理中,vue2 是通过对旧节点列表建立一个 { key, oldVnode }的映射表,然后遍历新节点列表的剩余节点,根据newVnode.key在旧映射表中寻找可复用的节点,然后打补丁并且移动到正确的位置。 vue3 则是建立一个存储新节点数组中的剩余节点在旧节点数组上的索引的映射关系数组,建立完成这个数组后也即找到了可复用的节点,然后通过这个数组计算得到最长递增子序列,这个序列中的节点保持不动,然后将新节点数组中的剩余节点移动到正确的位置