文章

Diff算法对比:Vue3 vs React 深度解析

Vue3 Diff 是双端比较四指针夹击,优先匹配头头尾尾头尾尾头;React Diff 是单向遍历 Fiber 链表,key 匹配复用不匹配跳出。 Vue 双端在列表反转场景占优,React 简洁换来可维护性与 Fiber 兼容,面试常考两者策略差异与各自适用场景。

Diff算法对比:Vue3 vs React 深度解析

一句话概括

Vue 3 的 Diff 是双端比较——四指针从数组两端夹击,优先匹配头头/尾尾/头尾/尾头四种快速路径;React 的 Diff 是单向遍历——从 Fiber 链表头部开始,key 匹配就复用、不匹配就跳出。Vue 的双端优势在列表反转这类场景明显,React 的简洁换来代码可维护性和 Fiber 架构兼容性。

核心知识点

1. Vue 3 双端比较:四个指针夹击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Vue 3 双端 Diff 简化核心
function patchKeyedChildren(c1, c2) {
  let i = 0;                          // 头指针
  let e1 = c1.length - 1;             // 旧数组尾指针
  let e2 = c2.length - 1;             // 新数组尾指针

  // ① 头头比较:新旧头部相同 → 复用,i++
  while (i <= e1 && i <= e2 && c1[i].key === c2[i].key) {
    patch(c1[i], c2[i]);
    i++;
  }
  // ② 尾尾比较:新旧尾部相同 → 复用,e1--, e2--
  while (i <= e1 && i <= e2 && c1[e1].key === c2[e2].key) {
    patch(c1[e1], c2[e2]);
    e1--; e2--;
  }
  // ③ 头尾比较:旧头 vs 新尾(节点被移到最后)
  // ④ 尾头比较:旧尾 vs 新头(节点被移到最前)
  // ⑤ 剩余用 key → index 映射 + LIS 算最少移动
}

四种快速路径覆盖了最常见的列表变化——追加、删除、反转、移动。第五步才用 LIS 算法处理复杂乱序。

2. React 单向遍历:到 key 不同就跳出

1
2
3
4
5
6
7
8
// React 第一轮(相当于 Vue 的头头比较,但更"脆弱")
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
  const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
  if (newFiber === null) break;  // key 不同 → 立刻跳出!
  // key 匹配 → 复用,继续下一个
  oldFiber = oldFiber.sibling;
}
// 跳出后剩余节点进第二轮 Map 匹配

React 没有双端能力,因为 Fiber 是单向链表——没有 prev 指针,无法从尾部向前遍历。

3. 列表反转场景的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
旧: A B C D E
新: E D C B A

Vue 3:
  头头: A vs E → 不匹配
  尾尾: E vs A → 不匹配
  头尾: A vs A → 匹配!(A 被移到末尾)i++, e2--
  尾头: E vs E → 匹配!(E 被移到开头)e1--, i++
  B vs D → 头尾匹配;D vs B → 尾头匹配
  共 4 次移动

React:
  第一轮: A vs E → key 不同,break
  剩余旧: [A,B,C,D,E],剩余新: [E,D,C,B,A]
  第二轮 Map 匹配: E(4)不移, D(3)移动, C(2)移动, B(1)移动, A(0)移动
  共 4 次移动

这个场景结果一样。但如果中间有插入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
旧: A B C D
新: A X B C D

Vue 3: 头头 A 匹配 → 剩余 [B,C,D] vs [X,B,C,D]
  → 构建 keyToIndex: {B:0,C:1,D:2}
  → X 不在 map 中 → 新建
  → B C D 的索引递增 → 无需移动
  共 1 次新建

React: A 匹配 → B vs X → key 不同 → break
  → Map 中查 X → 找不到 → 新建
  → 查 B → 找到 → oldIndex=1 >= lastPlacedIndex(0) → 不移
  → C D 同理
  共 1 次新建

日常场景两者差距微乎其微,远没有理论上的显著差异。

4. 最长递增子序列(LIS)

1
2
3
4
5
6
7
// Vue 3 用 LIS 算最少移动次数
function getSequence(arr) {
  // 返回 arr 中"最长递增子序列"的索引数组
  // 例如 [4,3,2,1] → LIS 长度 1, 索引 [0]
  //      [1,3,2,4] → LIS 长度 3, 索引 [0,1,3]
  // LIS 中的元素保持不变,其余的需要移动
}

React 用贪心 lastPlacedIndex 近似 LIS,在绝大多数场景结果一致,极端情况可能多 1-2 次 DOM 操作。这是工程取舍:代码复杂度降低 > 极偶尔的多一次移动。

5. key 在两者中的处理

1
2
3
4
5
6
Vue 3: key 用于双端匹配的快速判断,以及 LIS 阶段的新旧映射
React: key 用于第一轮的 updateSlot 匹配,以及第二轮的 Map 查找

共同点:无 key = 灾难。
  - Vue 3: 双端比较退化为顺序比较,大量错误复用
  - React: Map 用 index 做 key,删除中间项后全部错位

其实你每天都在用

  • 追加/删除尾部:两者的头头/尾尾比较都能在第一轮处理,路径最短
  • 反转/拖拽排序:Vue 的双端有天然优势,React 依赖第二轮 Map 才能处理
  • 表格按列排序:列表顺序全变,两者都不会在第一轮走完,都会进入中间处理阶段
  • key 是 ID vs 是 index:不管哪个框架,用 index 做 key 在删除/插入时都会导致大量错位复用

常见误解(FAQ)

  • ❌ 误区:「双端 Diff 总是比单端 Diff 快」 不是。头头+尾尾的快速路径确实好,但如果变化不在两端(如纯中间插入),双端多做的那几次比较就是纯开销。实际场景差距在 5% 以内。

  • ❌ 误区:「React 不用双端是因为水平不行」 是数据结构限制。Fiber 是单向链表,没有 prev 指针,无法从尾部向前遍历。React 团队选择了”更简单的更新模型换取可中断的并发渲染”这个更大目标。

  • ❌ 误区:「虚拟 DOM Diff 比直接操作 DOM 快」 经典迷思。直接 el.textContent = 'new' 比走虚拟 DOM 创建、Diff、最终 patch 快得多。虚拟 DOM 的价值是声明式编程体验 + 批量 DOM 操作优化,不是单次操作速度。

  • ❌ 误区:「两个框架的 Diff 都能跨层级比较」 都不能。同层级比较是虚拟 DOM Diff 的性能基础假设。跨层级移动元素(如从 body 移到 sidebar),两者都会销毁重建。

一句话总结

Vue 3 双端 Diff 像”两根筷子从两头夹”,React 单向 Diff 像”一根筷子从头拨”——速度差不多,脸不同而已。真正影响性能的不是 Diff 算法,而是你有没有给列表写对 key、有没有用虚拟滚动控制 DOM 数量。

本文由作者按照 CC BY 4.0 进行授权