vue2/3 diff算法

39 阅读6分钟

核心目标:最小化 DOM 操作

DOM 操作(创建、插入、删除节点)是浏览器中 最昂贵的操作之一 。Diff 算法的全部意义在于:通过 JavaScript 层面的计算(CPU),找出最小的差异,从而减少对真实 DOM(浏览器渲染引擎)的调用。

vue2

deepseek_mermaid_20260113_dc77fd.png Vue2 的 Diff 算法核心是 同级比较双端比较,它的完整流程可概括为以下步骤: deepseek_mermaid_20260113_f78956.png

算法核心:通过头头、尾尾、头尾、尾头四种直接比较,优先处理位置未变或简单移动的节点。只有这四种情况都不匹配时,才通过 key 映射表查找可复用节点,从而在 O(n)  时间复杂度内,以最少的 DOM 操作完成列表更新。

为什么是“双端”比较?(四个指针的策略)

这主要为了高效处理实际开发中最常见的数据变化场景,并为每种场景设计了最快捷的匹配路径:

  • 场景一:在列表头部/尾部添加或删除项(最常见)

    • 对应策略头头比较 和 尾尾比较

    • 为什么高效:例如在列表头部新增一项。旧列表[A,B,C] -> 新列表[D,A,B,C]

      1. 第一轮“头头比较”:旧A ≠ 新D,不匹配。
      2. 第一轮“尾尾比较”:旧C ≠ 新C?等一下,先比较尾部:旧C 对 新C?这里新列表的尾部是 C,旧列表尾部也是 C,匹配上了!算法会直接复用C节点并更新。
      3. 然后指针移动,继续“尾尾比较”:旧B 对 新B,再次匹配... 如此往复。
      4. 最终发现只有新节点 D 是新增的,只需一次 insertBefore 操作。
    • 总结头头/尾尾比较 能瞬间处理掉列表前后顺序保持一致的连续区间,快速缩小需要对比的“混乱”中间区域。

  • 场景二:列表反转或节点位置移动

    • 对应策略头尾比较 和 尾头比较

    • 为什么高效:例如反转列表。旧列表[A,B,C,D] -> 新列表[D,C,B,A]

      1. 头头比较:A ≠ D。
      2. 尾尾比较:D ≠ A。
      3. 头尾比较:旧头A 对 新尾A?匹配!  这意味着旧列表的头节点,应该移动到新列表的尾部。一次 insertBefore 操作就能将 A 移动到最后。
      4. 同理,下一轮 尾头比较 会发现旧尾D 匹配新头D,将D移动到最前。
    • 总结头尾/尾头比较 是处理节点位置交叉移动的快速通道。如果没有这两步,这种简单的反转操作会退化到需要靠 key 映射表来一个个查找和移动,效率更低。

为什么最后才用 key 映射?

前四种比较都是  “推测性”的快捷方式,它们基于一个假设:位置相近的节点更可能是同一个节点。这个假设在大多数用户操作(如增删首尾项、局部排序)下都是成立的,所以能快速解决大部分问题。

只有当四种快捷比较都失败时,才意味着中间部分可能存在复杂的乱序重排。这时,算法才启用“终极方案”:通过 key 建立的新旧节点映射表来精确查找。这是一种用空间(一个Map)换时间(O(1)查找)  的策略,确保了即使最混乱的情况也能正确处理。

vue3

deepseek_mermaid_20260113_95f14b.png

diff

deepseek_mermaid_20260113_dc77fd.png

Vue3 的 Diff 算法移除了 Vue2 中的“头尾比较”和“尾头比较”

在 Vue2 中,头尾/尾头比较主要处理的是  “节点位置交叉移动”  的场景:

// Vue2 头尾/尾头比较主要优化的场景:
旧列表: [A, B, C, D]
新列表: [D, A, B, C]  // 尾头比较会发现 D=D

旧列表: [A, B, C, D]
新列表: [B, C, D, A]  // 头尾比较会发现 A=A

问题在于:这种优化只在特定模式的移动中有效,对于更复杂的乱序场景帮助有限。

Vue3 的设计哲学转变

Vue3 的思考:与其用四种特殊比较处理部分场景,
不如用一个统一的强大算法处理所有场景

旧的思路(Vue2):
"让我们猜猜节点可能怎么移动,并为此设计快捷路径"
头头比较 → 节点位置没变
尾尾比较 → 节点位置没变  
头尾比较 → 节点从头部移到尾部
尾头比较 → 节点从尾部移到头部

新的思路(Vue3):
"让我们先找出所有确定不动的节点,然后一次性处理好所有移动"
1. 先快速找出首尾确定相同的节点(头头、尾尾)
2. 对中间乱序部分,建立完整映射,用LIS找出最少移动方案

Vue3 完全依赖 key 来识别节点身份,而 Vue2 的 sameVnode 除了 key 还考虑标签名等其他因素。

// Vue3 的节点标识更加严格
function isSameVNodeType(n1, n2) {
  return n1.type === n2.type && n1.key === n2.key
  // 只比较类型和key,比Vue2更简单但更依赖key
}

这意味着:
1. key 在 Vue3 中变得更加重要
2. 没有 key 或 key 不稳定时,Vue3 会直接创建新节点
3. 有了稳定的 key,Vue3 可以建立精确映射,不再需要猜测性比较

头尾/尾头比较是启发式算法(基于经验猜测),而 LIS(最长递增子序列)提供的是数学上的最优解

// 比较两种策略:
const strategies = {
  vue2: {
    approach: "启发式比较",
    movesForReverse: "n-1 次移动",
    optimal: "不是理论最少移动",
    complexity: "实现简单"
  },
  vue3: {
    approach: "LIS 优化",
    movesForReverse: "约 n/2 次移动",
    optimal: "接近理论最少移动",
    complexity: "实现复杂但更优"
  }
}

// 示例:8个节点的反转
// Vue2: 需要 7 次移动
// Vue3 (LIS): 只需要 4 次移动(优化 ~43%)

Vue3 的编译时优化减少了运行时需要 Diff 的场景(靶向更新):

// Vue3 编译时优化减少了乱序发生的可能性
const optimizations = [
  "静态提升(静态节点不参与Diff)",
  "Patch Flags(标记动态部分,跳过静态比较)",
  "树结构压平(减少嵌套Fragment的Diff层级)"
]

实际效果:很多在 Vue2 中需要完整 Diff 的场景,
在 Vue3 中可能被优化掉,根本不需要进入复杂 Diff

最长低增子序列 26.最长递增子序列概念_哔哩哔哩_bilibili