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
React的diff算法原理
React的diff算法原理
53 0
|
4天前
|
JavaScript 算法 开发者
vue diff算法介绍
vue diff算法介绍
15 2
|
2天前
|
JavaScript 前端开发 算法
React中的DOM diff算法是如何工作的
React的DOM diff算法通过对比新旧虚拟DOM树找到最小更新策略,提高组件更新效率。它生成并比较虚拟DOM,按类型、属性和&quot;key&quot;逐节点检查。不同类型节点直接替换,属性不同则更新属性,相同则递归比较子节点。确定DOM操作后批量执行,减少对真实DOM的访问,优化性能。然而,在复杂场景下可能有性能问题,可借助shouldComponentUpdate、memo或PureComponent等进行优化。
|
12天前
|
JavaScript 算法 前端开发
基于抽象语法树+diff算法实现Markdown编译器
基于抽象语法树+diff算法实现Markdown编译器
|
17天前
|
JavaScript 算法 对象存储
Vue是如何diff算法的
【4月更文挑战第24天】Vue 的 diff 算法核心是对比新旧虚拟 DOM 树,通过比较节点类型、属性及子节点,采用双指针策略和 key 判断,实现高效更新。当节点类型或属性变化时,Vue 更新或替换节点。子节点比较则尝试最小化 DOM 操作,通过 key 优化列表变更。算法递归处理组件和子节点,最终生成补丁对象来更新真实 DOM,提升性能。开发中,合理使用 key 和优化状态变化可进一步提升性能。
|
4月前
|
JavaScript 算法 前端开发
Vue diff 算法探秘:如何实现快速渲染
Vue diff 算法探秘:如何实现快速渲染
Vue diff 算法探秘:如何实现快速渲染
|
6月前
|
JavaScript 算法 前端开发
vue 中diff算法
vue 中diff算法
41 0
|
2月前
|
缓存 JavaScript 算法
Vue.js中的diff算法:让虚拟DOM更高效
Vue.js中的diff算法:让虚拟DOM更高效
|
4月前
|
JavaScript 算法 前端开发
解密Vue 2的Diff算法:如何实现高效的DOM更新?
解密Vue 2的Diff算法:如何实现高效的DOM更新?
|
9月前
|
算法 JavaScript
vue的diff算法?
vue的diff算法?