Vue3读源码系列(九):diff算法

72 阅读5分钟

上一章介绍了Block树的概念,最后说到当遇到不稳定Fragment的时候不会去使用动态子节点,而是会去使用children与老的vnode节点进行diff算法,我们上节说过使用v-for命令的节点会被Fragment包裹,所以进行更新patch时会执行processFragment,我们就从这个函数看起

processFragment

const processFragment = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  // fragment前后锚点 用于标记fragment的开始和结束 是两个空的文本节点
  const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
  const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!

  let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2

  if (
    __DEV__ &&
    // #5523 dev root fragment may inherit directives
    (isHmrUpdating || patchFlag & PatchFlags.DEV_ROOT_FRAGMENT)
  ) {
    // HMR updated / Dev root fragment (w/ comments), force full diff
    patchFlag = 0
    optimized = false
    dynamicChildren = null
  }

  // check if this is a slot fragment with :slotted scope ids
  // 检查是否是带有:slotted作用域id的插槽片段
  if (fragmentSlotScopeIds) {
    slotScopeIds = slotScopeIds
      ? slotScopeIds.concat(fragmentSlotScopeIds)
      : fragmentSlotScopeIds
  }

  if (n1 == null) {
    // insertBefore操作
    hostInsert(fragmentStartAnchor, container, anchor)
    hostInsert(fragmentEndAnchor, container, anchor)
    // a fragment can only have array children
    // since they are either generated by the compiler, or implicitly created
    // from arrays.
    // 挂载子节点
    mountChildren(
      n2.children as VNodeArrayChildren,
      container,
      fragmentEndAnchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  } else {
    if (
      patchFlag > 0 &&
      patchFlag & PatchFlags.STABLE_FRAGMENT && // 是否是稳定的Fragment
      dynamicChildren &&
      // #2715 the previous fragment could've been a BAILed one as a result
      // of renderSlot() with no valid children
      n1.dynamicChildren
    ) {
      // patchFlag是PatchFlags.STABLE_FRAGMENT(稳定的Fragment)且有dynamicChildren
      // 直接遍历dynamicChildren更新
      patchBlockChildren(
        n1.dynamicChildren,
        dynamicChildren,
        container,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds
      )
      if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
        traverseStaticChildren(n1, n2)
      } else if (
        // #2080 if the stable fragment has a key, it's a <template v-for> that may
        //  get moved around. Make sure all root level vnodes inherit el.
        // #2134 or if it's a component root, it may also get moved around
        // as the component is being moved.
        n2.key != null ||
        (parentComponent && n2 === parentComponent.subTree)
      ) {
        traverseStaticChildren(n1, n2, true /* shallow */)
      }
    } else {
      // 不稳定的Fragment,没有dynamicChildren,直接使用children去进行diff算法
      patchChildren(
        n1,
        n2,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }
}

我们只看不稳定的情况patchChildren

patchChildren

const patchChildren: PatchChildrenFn = (
  n1,
  n2,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds,
  optimized = false
) => {
  const c1 = n1 && n1.children
  const prevShapeFlag = n1 ? n1.shapeFlag : 0
  const c2 = n2.children

  const { patchFlag, shapeFlag } = n2
  // fast path
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.KEYED_FRAGMENT) { // 判断是否是带key的Fragment
      // this could be either fully-keyed or mixed (some keyed some not)
      // presence of patchFlag means children are guaranteed to be arrays
      // 对于有key的采用快速diff算法
      patchKeyedChildren(
        c1 as VNode[],
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      return
    } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
      // unkeyed
      // 对于没有key的采用非常简单的diff算法
      patchUnkeyedChildren(
        c1 as VNode[],
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      return
    }
  }

  // children has 3 possibilities: text, array or no children.
  // 其他三种类型children的处理:文本、数组、没有子节点
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // text children fast path
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
    }
    if (c2 !== c1) {
      hostSetElementText(container, c2 as string)
    }
  } else {
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // prev children was array
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // two arrays, cannot assume anything, do full diff
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        // no new children, just unmount old
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
      }
    } else {
      // prev children was text OR null
      // new children is array OR null
      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetElementText(container, '')
      }
      // mount new if array
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        mountChildren(
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      }
    }
  }
}

所以对于不稳定的Fragment一般会有两种情况,有key执行patchKeyedChildren,没有key执行patchUnkeyedChildren,我们先看简单的没有key的diff

没有key的diff算法:patchUnkeyedChildren

const patchUnkeyedChildren = (
  c1: VNode[], // 旧vnode children
  c2: VNodeArrayChildren, // 新vnode children
  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
  // 记录新旧vnode children的长度
  const oldLength = c1.length
  const newLength = c2.length
  const commonLength = Math.min(oldLength, newLength)
  let i
  // 遍历较短的那个children 直接按顺序patch
  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
    )
  }
  // 如果旧的节点比新的节点多,那么就把多余的节点删除 从commonLength开始
  if (oldLength > newLength) {
    // remove old
    unmountChildren(
      c1,
      parentComponent,
      parentSuspense,
      true,
      false,
      commonLength
    )
  } else { // 如果新的节点比旧的节点多,那么就把多余的节点挂载 从commonLength开始
    // mount new
    mountChildren(
      c2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized,
      commonLength
    )
  }
}

可以看到没有key的diff算法非常简单,下面来看有key的diff算法,也是我们常说的快速diff算法:patchKeyedChildren

快速diff算法:patchKeyedChildren

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 // 老子节点结束索引
  let e2 = l2 - 1 // 新子节点结束索引

  // 1. sync from start
  // (a b) c
  // (a b) d e
  // 从索引0开始比较,如果相同就更新,不同就跳出循环
  while (i <= e1 && i <= e2) {
    const n1 = c1[i]
    const n2 = (c2[i] = optimized
      ? cloneIfMounted(c2[i] as VNode)
      : normalizeVNode(c2[i]))
    // n1.type === n2.type && n1.key === n2.key
    // 如果vnode的type相同且key相同,则认为是同一个vnode 执行patch操作
    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
  // 如果i > e1 说明前面两步已经把所有旧节点都更新完了
  // 此时只需要把还未处理的新节点挂载到容器中即可
  if (i > e1) {
    if (i <= e2) {
      const nextPos = e2 + 1
      // 挂载的锚点是e2的下一个节点 如果该节点没有则是parentAnchor(一般是null)
      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
  // 如果i > e2 说明已经把所有新节点都更新完了 那么只需要卸载多余的旧节点即可
  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
    // 创建一个Map用来存储新子节点的key和索引的映射关系
    // 为后续遍历旧子节点时能快速根据key找到对应的新子节点的索引做准备 防止过高的时间复杂度 属于空间换时间的做法
    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
    // 5.2 遍历旧的子节点,尝试匹配新的子节点,匹配的到则patch更新,匹配不到则卸载节点
    let j
    let patched = 0
    // 剩余未处理新节点的数量
    const toBePatched = e2 - s2 + 1
    // 是否需要进行dom的移动操作
    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
    // 声明一个newIndexToOldIndexMap数组
    // 数组的索引是剩余新节点的索引(从0开始)
    // 值是对应节点在所有旧节点中的位置(这里的位置是指在所有旧节点中排第几个index+1,避免0索引与默认值0冲突)
    const newIndexToOldIndexMap = new Array(toBePatched)
    // 使用0填充数组 0表示新节点没有对应的旧节点 最后需要执行创建节点的操作
    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
        // 所有的新节点都已经patch更新了 所以这里只能是卸载节点
        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
        // 如果旧节点缺少key 则遍历未处理新节点寻找相同类型的节点
        for (j = s2; j <= e2; j++) {
          if (
            newIndexToOldIndexMap[j - s2] === 0 &&
            isSameVNodeType(prevChild, c2[j] as VNode)
          ) {
            newIndex = j
            break
          }
        }
      }
      // 如果没有获取到新节点的索引 则卸载prevChild
      if (newIndex === undefined) {
        unmount(prevChild, parentComponent, parentSuspense, true)
      } else {
        // 存在对应的新节点则更新 新节点索引和旧节点位置的映射关系
        newIndexToOldIndexMap[newIndex - s2] = i + 1
        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
    // 5.3 移动和挂载
    // 只有dom需要移动时(moved为true)才生成最长递增子序列
    // 根据newIndexToOldIndexMap获取最长递增子序列increasingNewIndexSequence
    // 最长递增子序列是一个由newIndexToOldIndexMap索引填充的数组 数组中对应索引的新节点不用移动
    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
      // 为0表示是新节点 挂载新节点
      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
        // 判断i是否等于最长递增子序列的最后一个元素 如果不等于则移动节点 否则不移动
        if (j < 0 || i !== increasingNewIndexSequence[j]) {
          move(nextChild, container, anchor, MoveType.REORDER)
        } else {
          j--
        }
      }
    }
  }
}

快速diff算法难理解的地方可能在于最长递增子序列,为什么最长递增子序列对应的新节点不需要移动?这其实很好理解,这里利用了相对位置的概念。试想一下一些节点他们之间的相对位置没有改变,那么他们是不是不需要移动?我们是不是只需要移动节点到正确的位置就可以了?所以我们要找出最长的相对位置没有发生改变的节点数组,以此来移动最少数量的节点,以节省性能。
PS:对于快速diff算法最好写一个简单的示例debugger一下比较好,因为其中一些对于索引的处理还是比较绕,debugger走一遍会让思路更加清晰。下面提供一个小示例,供大家调试:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <div v-for="item in arr" :key="item">{{ item }}</div>
    <button @click="changeArr">change</button>
  </div>
  <script src="../../dist/vue.global.js"></script>
  <script>
    const { createApp, ref } = Vue
    createApp({
      setup() {
        const arr = ref(['a', 'b', 'c', 'd', 'e', 'f', 'g'])
        const changeArr = () => {
          arr.value = ['a', 'b', 'e', 'c', 'd', 'h', 'f', 'g']
        }
        return {
          arr,
          changeArr
        }
      }
    }).mount('#app')
  </script>
</body>
</html>