Vue3源码之diff算法的核心 patchKeyedChildren

476 阅读2分钟

背景

研究一下Vue3 diff 源码,逐条加了注释,清晰明白,

source code patchKeyedChildren

/* 
    Vue3 diff 算法的核心
  */
  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 更新节点 */
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        /* 结束while循环 */
        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,更新节点 */
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        /* 结束while循环 */
        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
    /* 4. 旧子节点,有剩余要删除的多余节点 */
    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 {
      /* 旧子序列开始索引,从 i 开始记录 */
      const s1 = i // prev starting index
      /* 新子序列开始索引,从 i 开始记录 */
      const s2 = i // next starting index

      /* 
        5.1 build key:index map for newChildren
        根据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.`
            )
          }
          /* key 对应 i */
          keyToNewIndexMap.set(nextChild.key, i)
        }
      }

      /* 
        5.2 循环遍历 旧子序列 patch
      */
      let j
      let patched = 0
      const toBePatched = e2 - s2 + 1
      let moved = false
      /* 用于跟踪判断是否有节点移动 */
      let maxNewIndexSoFar = 0
      /* 这个数组用于存储 新子序中的元素在旧子序列节点的索引,用于确定,最长递增子序列 */
      const newIndexToOldIndexMap = new Array(toBePatched)
      /* 初始化数组,每个元素都是0, 0 是一个特殊的值,如果遍历完了,仍然有元素的值为0, 
      则说明这个新节点没有对应的旧节点 */
      for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
      /* 循环遍历 旧子节点 */
      for (i = s1; i <= e1; i++) {
        /* 每一个旧子序列节点 */
        const prevChild = c1[i]
        /* toBePatched 代表新子序列的长度 */
        if (patched >= toBePatched) {
          /* 所有的新的子序列的节点都已经更新,剩余的旧子序列的节点删除 */
          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 {
          /* 更新,新子序列的元素 在旧子序列中的索引
            这里加上1 偏移, 是为了避免i为0的特殊情况,影响对后续 最长递增子序列的求解
          */
          newIndexToOldIndexMap[newIndex - s2] = i + 1
          /* maxNewIndexSoFar 始终存储的是上次求值的newIndex,如果不是一直递增,则说明有移动 */
          if (newIndex >= maxNewIndexSoFar) {
            maxNewIndexSoFar = newIndex
          } else {
            moved = true
          }
          /* 更新新旧子序列中匹配的节点 */
          patch(
            prevChild,
            c2[newIndex] as VNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          patched++
        }
      } /* 循环遍历旧节点 END */

      // 5.3 move and mount
      /* 
        moved 为TRUE 说明有移动
        getSequence 计算最长递增子序列, 这是最复杂的算法
        newIndexToOldIndexMap:  [5, 3, 4, 0] 里面存的值 是旧的子序列的索引
        5 要移动到4的后面,0占位,需要新增一个节点
        假如 newIndexToOldIndexMap 为 [5, 3, 4, 0] 那么 最长递增子序列就是 [1,2] 里面存的是索引
      */
      const increasingNewIndexSequence = moved
        ? getSequence(newIndexToOldIndexMap)
        : EMPTY_ARR
      j = increasingNewIndexSequence.length - 1
      /* 
      使用倒序的方式,遍历 toBePatched 是新序列中的要对比的部分的长度 
      方便我们使用最后更新的节点作为锚点
      5.2是遍历了旧的,这一次要遍历新的
      */
      for (i = toBePatched - 1; i >= 0; i--) {
        const nextIndex = s2 + i
        const nextChild = c2[nextIndex] as VNode
        /* 锚点指向上一个更新的节点,如果 nextIndex 超过新子节点的长度,则指向parentAnchor */
        const anchor =
          nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
        if (newIndexToOldIndexMap[i] === 0) {
          /* 0 是占位,mount 挂载新节点  */
          patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (moved) {
          /* 
          没有最长递增子序列(reverse的场景)或者当前的节点索引不在最长递增子序列中,需要移动
          移动到上一次操作节点的前面
          */
          if (j < 0 || i !== increasingNewIndexSequence[j]) {
            move(nextChild, container, anchor, MoveType.REORDER)
          } else {
            j--
          }
        }
      }
    }
  } /* patchKeyedChildren end */

getSequence

计算最长递增子序列, 里面最复杂的算法

/* 计算最长递增子序列
  贪心+ 二分查找
  贪心是O(n)
  二分查找是O(logn)
  总的是 时间复杂度 O(logn)
*/
// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
function getSequence(arr: number[]): number[] {
  const p = arr.slice()
  const result = [0]
  let i, j, u, v, c
  const len = arr.length
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      j = result[result.length - 1]
      if (arr[j] < arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      u = 0
      v = result.length - 1
      while (u < v) {
        c = (u + v) >> 1
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1]
        }
        result[u] = i
      }
    }
  }
  u = result.length
  v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}