解析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--
    }
  }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值