虚拟 DOM 与 Diff 算法

1. 为什么需要 Virtual DOM?

很多人以为 Virtual DOM (虚拟 DOM) 是为了“快”,其实不全对。

(1) 核心价值:跨平台

Virtual DOM 本质上是一个 JavaScript 对象。因为它不依赖于浏览器的 DOM API,所以它可以被渲染到任何地方:

  • Web: ReactDOM / Vue Runtime Dom
  • 移动端: React Native / Weex
  • 小程序: Taro / UniApp

(2) 核心价值:声明式编程

它让开发者不需要手动操作 DOM(如 jQuery 时代),只需要关注状态。框架会自动通过 Diff 算法计算出最小的变更并应用到真实 DOM 上。

(3) 关于性能

  • 首次渲染:Virtual DOM 因为多了一层计算,其实比直接操作原生的 innerHTML
  • 更新渲染:在大量数据更新时,Virtual DOM 可以通过 Diff 算法避免不必要的全量重绘,比 innerHTML 全量替换要
  • 结论:Virtual DOM 保证了性能的下限,让你在不进行手动优化的情况下也能获得不错的性能。

2. Diff 算法原理

Diff 的目标是找出两个树结构的最小变更。

  • 传统树 Diff 复杂度:O(n^3)。(需要遍历、比较、修正)。
  • React/Vue 优化后的复杂度O(n)

O(n) 的秘诀 (三大假设)

  1. 分层对比:只对比同级元素,不跨级比较。如果一个节点跨层级移动了,框架会直接销毁并重新创建,而不是移动它。
  2. 类型判断:如果两个节点的标签类型不同 (如 divp),直接销毁重建整个子树,不再去比较子节点。
  3. Key 标识:对于同一层级的子节点列表,通过唯一的 key 来区分。

3. React vs Vue 的 Diff 区别

React (Fiber Diff)

  • 单节点 diff:比较 type 和 props。
  • 多节点 diff:两轮遍历。第一轮处理更新/删除;第二轮处理新增/移动。
  • 核心策略右移策略。React 只会把节点向右移动。如果一个节点本该在左边但现在的下标比较小,它不动,而是更新。

Vue 2 (双端 Diff)

  • 策略:使用 4 个指针(旧头、旧尾、新头、新尾)。
  • 过程:互相进行比较(头头、尾尾、头尾、尾头)。
  • 优点:能更好地处理节点的移动(尤其是反转列表),比 React 的单向移动策略复用率更高。

Vue 3 (快速 Diff)

  • 策略:最长递增子序列 (LIS)。
  • 过程
    1. 先处理头部的相同节点和尾部的相同节点。
    2. 对剩余的乱序节点,生成一个 source 数组,计算最长递增子序列
  • 优势:LIS 算法能计算出最少的移动次数,性能最好。

4. 为什么列表需要 Key?

Key 是节点的唯一标识

没有 Key (就地复用)

例如:[A, B, C] -> [A, C, B] React/Vue 会认为第二个位置从 B 变了 C,第三个位置从 C 变了 B,于是会去更新这两个组件的内容,而不是移动DOM节点

  • 后果:如果组件有状态(如输入框里有值),状态会错乱(C 继承了 B 的输入框内容)。

有 Key

框架知道 Key 为 B 的节点只是移动了位置,直接移动 DOM 节点,不需要重新渲染内容,性能更高且状态安全。

切记

不要用 index 做 key(除非列表是静态的)。一旦列表顺序变化,Index 全变了,Key 就失去了意义,导致大量不必要的更新或状态错乱。

5. Vue 3 的编译时优化

除了 Diff 算法优化,Vue 3 最大的杀器是编译时优化

  • Patch Flags:编译模板时,给动态节点打标记(如 Text, Class, Style)。Diff 时只对比有标记的节点,忽略静态节点。
  • Hoist Static:静态节点提升,只创建一次。
  • Block Tree:配合 Patch Flags,实现了 Diff 性能与模板大小无关,只与动态节点数量相关。这是 React 做不到的(React 是完全运行时的)。