Vuejs设计与实现 —— 渲染器核心 Diff 算法(下)

简介: Vuejs设计与实现 —— 渲染器核心 Diff 算法

快速 Diff 算法

Vue.js 3 借鉴了 iviinferno 这两个框架中使用的算法:快速 Diff 算法,这个算法的性能优于 Vue.js 2 中所采用的 双端 Diff 算法.

以下涉及的源码位置均在:vue-core-3.2.31-main\packages\runtime-core\src\renderer.ts 中的 patchKeyedChildren 函数中

节点预处理

对于 相同位置前置节点后置节点,由于它们在新旧两组子节点中的相对位置不变,因此并 不需要 进行 移动 操作,只需 进行 patch更新 即可.

处理前置节点

通过开启一个 while 循环,从前往后 依次遍历新旧两组子节点:

  • 若当前新旧节点 相同,则通过 patch 进行 更新
  • 若当前新旧节点 不同,则终止循环,即前置节点处理结束

Vue.js 3 中对应源码如下:

// 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++
}
复制代码

处理后置节点

通过开启一个 while 循环,从后往前 依次遍历新旧两组子节点:

  • 若当前新旧节点 相同,则通过 patch 进行 更新
  • 若当前新旧节点 不同,则终止循环,即后置节点处理结束

Vue.js 3 中对应源码如下:

// 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--
}
复制代码

处理剩余已知公共序列的节点

当完成 节点预处理 后,很可能出现以下两种情况,而这些剩余节点是很容易根据已处理过的前后节点推断出它们的具体位置的:

  • 旧节点遍历完成,新节点有剩余,此时意味着有新节点需要挂载,通过 patch 将剩余新节点依次进行 挂载
  • 新节点遍历完成,旧节点有剩余,此时意味着有旧节点需要卸载,通过 unmount 将剩余旧节点依次进行 卸载

image.png

Vue.js 3 中对应源码如下:

// 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++
  }
}
复制代码

处理剩余未知序列的节点

直接来看 vue.js 3 在源码中举的例子:

旧节点:  [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
复制代码

其中,经过 节点预处理 后的剩余节点,即 [c d e][e d c h] 的部分是乱序的,针对这部分节点的处理是很关键的:

  • 通过 toBePatched 保存新节点的数量,即 toBePatched = e2 - s2 + 1
  • 基于 newChildren 的剩余节点,构造基一个形如 key: indexkeyToNewIndexMap 索引映射,本质是一个 Map 对象
  • 遍历旧的一组节点中剩余为处理的节点,进行 patch 更新或 unmount 卸载
  • 若当前遍历的 老节点的 key 能在 keyToNewIndexMap 中获取到对应的索引值,则说明当前节点是可复用的节点,可通过 patch 进行 更新,并通过 patched 记录下当前已 被更新/被复用 的节点数
  • 若当前遍历的 老节点的 key 不能在 keyToNewIndexMap 中获取到对应的索引值,则说明当前的老节点通过 unmount 进行卸载
  • patched >= toBePatched,则说明所有剩余的新节点都已经在剩余旧节点中找到并更新完成,此时需要对旧节点中剩余老节点通过 unmount 进行卸载
  • 若当前老节点对应新节点中的索引 小于 上一次记录的索引值,则说明当前节点需要移动,将 moved 变量标识为 true,便于后续基于 最长递增子序列 进行 移动 操作
  • 通过以上操作后,可以通过构造一个 最长的稳定子序列 用于后续节点的 移动 操作,即 最长递增子序列算法
  • 通过构建 newIndexToOldIndexMap 数组,用于存储 当前新节点老节点中 的索引值
  • 基于 newIndexToOldIndexMap 数组通过 getSequence(newIndexToOldIndexMap) 得到最长递增子序列,其中相关算法感兴趣的可自行研究
  • 从后往前 遍历,其中索引 i 指向新的一组子节点的最后一个节点,而索引 j 指向的是最长递增子序列中的最后一个元素
  • 若当前新节点对应老节点中的索引为 0,则说明当前节点需要进行 挂载
  • movedtrue 则说明当前新节点需要进行 移动

Vue.js 3 中对应源码如下:

// 5.3 move and mount
  // generate longest stable subsequence only when nodes have moved
  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
    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
      if (j < 0 || i !== increasingNewIndexSequence[j]) {
        move(nextChild, container, anchor, MoveType.REORDER)
      } else {
        j--
      }
    }
  }
复制代码

完结


目录
相关文章
|
1月前
|
监控 JavaScript 前端开发
Vue 异步渲染
【10月更文挑战第23天】Vue 异步渲染是提高应用性能和用户体验的重要手段。通过理解异步渲染的原理和优化策略,我们可以更好地利用 Vue 的优势,开发出高效、流畅的前端应用。同时,在实际开发中,要注意数据一致性、性能监控和调试等问题,确保应用的稳定性和可靠性。
|
2月前
|
算法 JavaScript UED
Diff 算法的实现原理
【10月更文挑战第18天】Diff 算法是 Vue.js 中实现高效 DOM 更新的核心机制,通过合理的比较和优化策略,能够在保证界面正确性的同时,最大程度地减少 DOM 操作,提高应用的性能和用户体验。
36 2
|
2月前
|
算法 JavaScript
Vue 中的 Diff 算法
【10月更文挑战第18天】需要注意的是,Diff 算法虽然能够提高性能,但在某些复杂的场景下,可能仍然会存在一些性能瓶颈。因此,在实际开发中,我们需要根据具体情况合理地使用 Diff 算法,并结合其他优化手段来提高应用的性能。
19 1
|
2月前
|
JavaScript 算法 前端开发
vue 中diff算法
【10月更文挑战第10天】
40 1
|
2月前
|
JavaScript 算法 前端开发
【VUE】Vue的diff算法和React的diff算法
【VUE】Vue的diff算法和React的diff算法
|
7月前
|
JavaScript 算法 前端开发
Vue diff 算法探秘:如何实现快速渲染
Vue diff 算法探秘:如何实现快速渲染
Vue diff 算法探秘:如何实现快速渲染
|
4月前
|
JavaScript 算法 前端开发
"揭秘Vue.js的高效渲染秘诀:深度解析Diff算法如何让前端开发快人一步"
【8月更文挑战第20天】Vue.js是一款备受欢迎的前端框架,以其声明式的响应式数据绑定和组件化开发著称。在Vue中,Diff算法是核心之一,它高效计算虚拟DOM更新时所需的最小实际DOM变更,确保界面快速准确更新。算法通过比较新旧虚拟DOM树的同层级节点,递归检查子节点,并利用`key`属性优化列表更新。虽然存在局限性,如难以处理跨层级节点移动,但Diff算法仍是Vue高效更新机制的关键,帮助开发者构建高性能Web应用。
80 1
|
JavaScript 算法 前端开发
vue 中diff算法
vue 中diff算法
76 0
|
算法 JavaScript
vue的diff算法?
vue的diff算法?
|
7月前
|
缓存 JavaScript 算法
Vue.js中的diff算法:让虚拟DOM更高效
Vue.js中的diff算法:让虚拟DOM更高效
下一篇
DataWorks