虚拟 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) 的秘诀 (三大假设)
- 分层对比:只对比同级元素,不跨级比较。如果一个节点跨层级移动了,框架会直接销毁并重新创建,而不是移动它。
- 类型判断:如果两个节点的标签类型不同 (如
div 变 p),直接销毁重建整个子树,不再去比较子节点。
- Key 标识:对于同一层级的子节点列表,通过唯一的
key 来区分。
3. React vs Vue 的 Diff 区别
React (Fiber Diff)
- 单节点 diff:比较 type 和 props。
- 多节点 diff:两轮遍历。第一轮处理更新/删除;第二轮处理新增/移动。
- 核心策略:右移策略。React 只会把节点向右移动。如果一个节点本该在左边但现在的下标比较小,它不动,而是更新。
Vue 2 (双端 Diff)
- 策略:使用 4 个指针(旧头、旧尾、新头、新尾)。
- 过程:互相进行比较(头头、尾尾、头尾、尾头)。
- 优点:能更好地处理节点的移动(尤其是反转列表),比 React 的单向移动策略复用率更高。
Vue 3 (快速 Diff)
- 策略:最长递增子序列 (LIS)。
- 过程:
- 先处理头部的相同节点和尾部的相同节点。
- 对剩余的乱序节点,生成一个 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 是完全运行时的)。