乐趣区

关于vue3:解析Vue3patch核心算法patchKeyedChildren

解析 Vue3patch 外围算法 patchKeyedChildren

  • locate:runtime-core > renderer > baseCreateRenderer > patchKeyedChildren
  • patchKeyedChildren 是 patch 算法中较为简单的一段,更是 vue 技术栈面试的高频点,最后我是在 vue3.0 beta 版本时大抵看了源码,起初在《vue 设计与剖析》中看了霍春阳大佬的解析,本篇就简要剖析一下做个记录
  • 首先 patchKeyedChildren 是在子列表比照并且有 key 的状况会进入,并且逻辑大抵分为 5 步,这个看源码官网正文就能够看出

第一步,从前向后遍历

这一步是从节点组头部向尾部遍历,如果遍历过程中遇到类似节点,就进行 patch 比照,否则就退出遍历,并记录以后遍历的最新下标

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++
}

第二步,从后向前遍历

从后向前遍历,如果遇到第一步记录的下标就进行,而后遍历过程中,如果遇到类似节点也是间接进行 patch 比照,如果不雷同就是间接退出遍历,并且记录旧节点组和新节点组的尾指针

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--
}

第三步,查看旧节点组

这一步就是查看旧节点组在上两步的遍历后是否遍历完,如果遍历完,那么新节点组没有遍历完的就都是新的 dom,能够全副当作新增节点进行挂载解决

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,就是代表没有旧节点,间接将新节点插入
      patch(
        null,
        (c2[i] = optimized
          ? cloneIfMounted(c2[i] as VNode)
          : normalizeVNode(c2[i])),
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      i++
    }
  }
}

第四步,查看新节点组

如果上一步查看旧节点未遍历完,那么就查看新节点组是否遍历完,如果遍历完,那么旧的节点组残余的节点阐明都是要卸载的,因为都不须要了

else if (i > e2) {
  // 旧子节点未被遍历完
  while (i <= e1) {unmount(c1[i], parentComponent, parentSuspense, true)
    i++
  }
}

第五步,未知序列

  • 如果新旧节点组都未遍历完,阐明存在未知序列,可能存在位移等状况,就须要进一步解决
  • 首先创立一个数组,用于记录新旧节点的对应关系

    // toBePatched 是新序列的节点数量 e2 - s2 + 1
    const newIndexToOldIndexMap = new Array(toBePatched)
    for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
  • 而后会遍历旧节点组,这里会用两个变量记录

    • let moved = false:位移标识,用于判断是否须要位移
    • let patched = 0:记录已执行 patch 的新节点数量,用于解决如果在更新时更新过的数量大于须要更新的节点数量,就卸载对应旧节点

      for (i = s1; i <= e1; i++) {const prevChild = c1[i]
      // 如果已更新数量大于新节点数量,就卸载节点
      if (patched >= toBePatched) {unmount(prevChild, parentComponent, parentSuspense, true)
      continue
      }
      let newIndex // 新旧节点 key 雷同的新节点 index
      if (prevChild.key != null) {newIndex = keyToNewIndexMap.get(prevChild.key)
      } else {for (j = s2; j <= e2; j++) {
        if (newIndexToOldIndexMap[j - s2] === 0 &&
          isSameVNodeType(prevChild, c2[j] as VNode)
        ) {
          newIndex = j
          break
        }
      }
      }
      // 如果 newIndex 为空,则阐明未找到与旧节点对应的新节点,间接卸载
      if (newIndex === undefined) {unmount(prevChild, parentComponent, parentSuspense, true)
      } else {
      // 更新新旧节点关系表
      newIndexToOldIndexMap[newIndex - s2] = i + 1
      /**
       * 这里的 maxNexIndexSoFar 是记录每次 patch 最大 index
       * 也是用于判断是否产生了位移,因为如果新节点 index 比最大 index 小,就阐明产生了位移
       * 例如:*  (a b) c
       *  (a c  b)
       * 在新旧序列比照 b,c 时,因为 c 最新的 newIndex 曾经小于 b 对应的 newIndex,因而会记录须要位移
       */
      if (newIndex >= maxNewIndexSoFar) {maxNewIndexSoFar = newIndex} else {moved = true}
      patch(
        prevChild,
        c2[newIndex] as VNode,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      patched++
      }
      }
  • 遍历完旧序列后,就须要确定如何位移
  • 首先是依据新旧节点关系表生成最长递增子序列 (这个算法就不陈说了,较简单,能够看精读《DOM diff 最长回升子序列》),而后倒序遍历新子节点

    // 最长递增子序列
    const increasingNewIndexSequence = moved
    ? getSequence(newIndexToOldIndexMap)
    : EMPTY_ARR
    j = increasingNewIndexSequence.length - 1
    // 从新序列尾部向前遍历,目标是为了应用上一个遍历的节点做锚点
    for (i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i
    const nextChild = c2[nextIndex] as VNode
    // 如果是序列的最初一个节点,anchor 就是父节点对应的 anchor,否则就是上一个子节点
    const anchor =
      nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
    // 如果 newIndexToOldIndexMap[i] 还是 0,那么阐明新节点没有对应的旧节点,阐明是新节点,间接挂载
    if (newIndexToOldIndexMap[i] === 0) {
      patch(
        null,
        nextChild,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else if (moved) {
      // 而后在 moved 为 true 的状况下,如果最长子序列为空或者最长子序列和以后拜访的新节点 index 不同,阐明就须要进行位移
      if (j < 0 || i !== increasingNewIndexSequence[j]) {move(nextChild, container, anchor, MoveType.REORDER)
      } else {j--}
    }
    }
退出移动版