虚拟 DOM 和 DIFF 算法
约 1204 字大约 4 分钟
2026-03-09
什么是虚拟 DOM?
虚拟DOM 本质上是一个 用 JavaScript 对象来描述真实 DOM 结构 的数据结构。
- 真实 DOM: 浏览器提供的庞大、复杂的对象树,操作成本高(触发重排 Reflow 和重绘 Repaint)。
- 虚拟 DOM: 轻量级的 JS 对象,操作成本极低。
真实 HTML:
<div id="app" class="container">
<h1>Hello</h1>
</div>虚拟 DOM (简化版):
const vnode = {
tag: 'div',
props: { id: 'app', class: 'container' },
children: [
{ tag: 'h1', props: {}, children: ['Hello'] }
]
};虚拟 DOM 的核心思想是状态驱动视图。当数据变化时,我们不直接操作真实 DOM,而是生成新的虚拟 DOM 树,通过对比差异,最小化地更新真实 DOM。
为什么需要虚拟 DOM?
很多人误以为虚拟 DOM 是为了“快”,其实不完全准确。虚拟 DOM 的核心价值在于“保证性能下限”和“开发体验”。
- 性能缓冲:
- 直接操作 DOM 是昂贵的。如果你循环 1000 次修改 DOM,浏览器可能重排 1000 次。
- 虚拟 DOM 允许我们在 JS 层完成所有计算,最后一次性批量更新到真实 DOM。
- 跨平台能力:
- 虚拟 DOM 只是 JS 对象。它可以映射到 Web DOM,也可以映射到 Native 组件(React Native)。
- 声明式编程:
- 开发者只需关心
state -> UI的映射,无需关心具体的 DOM 操作步骤(如appendChild,removeChild)。
- 开发者只需关心
在极端性能敏感的场景(如高频动画),直接操作 DOM 依然比虚拟 DOM 快。虚拟 DOM 是用少量的性能损耗换取了开发效率和可维护性。
Diff 算法:如何高效更新?
如果每次数据变化都重新渲染整棵树,性能依然很差。Diff 算法 的目的就是找出两棵虚拟 DOM 树的最小差异。
理论上,对比两棵树的完全差异算法复杂度是 O(n^3)(n 是节点数)。前端框架通过以下 三大策略 将复杂度降低到 O(n):
同层比较
- 规则: 只对比同一层级的节点,不会跨层级对比。
- 原因: 前端 UI 中,跨层级移动 DOM 节点的情况极少。
- 操作: 如果发现节点类型不同,直接销毁旧节点,创建新节点,不再深入对比子节点。
// 旧 <div><span>Hi</span></div> // 新 <div><p>Hi</p></div> // Diff 结果:span 和 p 类型不同,直接替换整个子节点,不对比内部内容。
类型比较
- 规则: 对比节点类型(标签名、组件构造函数)。
- 操作:
- 类型相同:继续对比属性和子节点。
- 类型不同:直接销毁旧树,重建新树。
Key 值比较
场景: 当子节点是列表时,如何判断哪个节点是新增、删除还是移动?
规则: 通过
key属性唯一标识节点。操作:
- 有 Key: 框架建立
key -> 节点的映射。如果 key 存在但位置变了,执行移动操作;如果 key 不存在,执行新增/删除。 - 无 Key (或 index 作为 key): 框架倾向于就地更新。如果列表顺序变化,可能导致状态错乱(如输入框内容保留但对应数据变了)。
// 错误示范:使用 index 作为 key // 删除第一项后,第二项的 key 变成 0,React 以为它是原来的第一项,导致状态复用错误 {list.map((item, index) => <Item key={index} data={item} />)} // 正确示范:使用唯一 ID {list.map((item) => <Item key={item.id} data={item} />)}- 有 Key: 框架建立
框架实现差异
虽然核心思想一致,但 React 和 Vue 在实现上有显著不同。
| 特性 | React (Fiber 架构) | Vue 2 / Vue 3 |
|---|---|---|
| Diff 触发 | 状态更新即触发 (自顶向下) | 依赖收集触发 (组件级更新) |
| 数据结构 | 纯 JS 对象 (Fiber 节点) | 带标记的 JS 对象 |
| 递归方式 | 可中断递归 (Fiber 链表) | 同步递归 (Vue 2) / 编译优化 (Vue 3) |
| 列表 Diff | 双端比较 (React 15) -> 单端遍历 (React 18+) | 双端比较 (Vue 2) -> 最长递增子序列 (Vue 3) |
| 优化手段 | 手动优化 (React.memo, useMemo) | 编译时优化 (静态提升、补丁标志) |
深度解析:
- React Fiber: 为了解决大组件更新阻塞主线程的问题,React 16 引入了 Fiber。它将递归遍历改为链表遍历,允许在 Diff 过程中暂停、恢复,优先处理高优先级任务(如用户输入),实现时间切片 (Time Slicing)。
- Vue 3 编译优化: Vue 3 不仅仅是运行时优化,还引入了编译器。编译器可以静态分析模板,将静态节点提升(Static Hoisting),并在动态节点上添加
patchFlag(补丁标志)。Diff 时只对比带有标志的动态节点,跳过静态内容,速度极快。