在 13.5 节中,我们提到使用文档碎片(DocumentFragment)可以将多次 DOM 操作合并为一次,从而减少重排和重绘。这是直接操作 DOM 时的一种优化手段。而现代前端框架(如 React、Vue)普遍采用了一种更底层的策略——虚拟 DOM(Virtual DOM),它彻底改变了开发者与 DOM 打交道的方式。
13.6.1 虚拟 DOM 是什么
虚拟 DOM 本质上就是用普通的 JavaScript 对象来描述真实 DOM 结构。一个 DOM 节点可以用一个包含标签名、属性、子节点的对象来表达。
// 真实 DOM:<div id="app"><p>Hello</p></div>
// 对应的虚拟 DOM 对象可能长这样:
const vnode = {
tag: 'div',
props: { id: 'app' },
children: [
{
tag: 'p',
props: {},
children: ['Hello']
}
]
};
这个对象就是所谓的“虚拟节点”(vnode)。它极其轻量,不包含真实 DOM 上那些庞大的属性和方法,创建和比较的成本远低于操作真实 DOM。
13.6.2 为什么需要虚拟 DOM:直接操作 DOM 的痛点
浏览器中的 DOM 是树状结构,每一次修改都可能触发重排(回流)或重绘,而这些操作的代价非常昂贵。如果我们在一个循环里逐个更新 DOM 节点,浏览器会不断重新计算布局、重新绘制页面,性能直接崩溃。
早期的开发者会使用文档碎片或 innerHTML 来批量更新,但这本质上是“手动”的优化,需要时刻注意什么时候该合并操作、什么时候该触发更新。随着应用状态越来越复杂、交互越来越频繁,这种手动优化变得力不从心,且容易引入无可预料的 bug。
虚拟 DOM 的出现解决了一个核心问题:让开发者不再需要手动去比对 DOM 并进行最低成本的更新,而是用声明式的方式描述界面,由框架自动完成高效的 DOM 更新。
13.6.3 虚拟 DOM 的工作流程
虚拟 DOM 的运转通常分为三个步骤:
- 生成虚拟 DOM 树
当应用状态发生变化时,框架会根据新的状态生成一棵全新的虚拟 DOM 树。
- Diff 比对
用旧虚拟 DOM 树和新虚拟 DOM 树进行比较(称为 diff 算法),找出哪些节点真正需要更新、哪些可以复用、哪些需要删除或新增。比对过程完全在 JavaScript 内存中进行,速度极快。
- 打补丁(Patch)
根据 diff 结果,生成最小的 DOM 操作集,一次性(或批量)应用到真实 DOM 上。浏览器只需执行这一次集中的更新,极大减少了重排重绘。
整个过程可以概括为:
状态变更 → 生成新虚拟 DOM → 与旧虚拟 DOM 对比 → 计算出最少变更 → 批量更新真实 DOM
13.6.4 虚拟 DOM 的核心价值
1. 性能:将多次 DOM 操作合并为一次最优更新
虚拟 DOM 并不是天生比手动操作 DOM 快——如果你直接操作 DOM,且操作非常精巧、针对性强,是可能比虚拟 DOM 更快的。但虚拟 DOM 的价值在于,它能自动找到最优的 DOM 更新策略,而无需开发者自己编写冗长的比对逻辑。在复杂的动态界面中,这极大地降低了性能优化的门槛。
2. 开发体验:声明式 UI,专注状态而非 DOM
使用虚拟 DOM 后,开发者不再需要手动维护 DOM 状态和 UI 状态的对应关系。你只需描述“界面应该是什么样子的”,框架负责把状态转换为 DOM 并高效更新。
// 不用虚拟 DOM:手动获取节点、判断、修改
if (user.name !== oldName) {
document.getElementById('name').textContent = user.name;
}
// 使用虚拟 DOM(以 React 为例):直接声明 UI 与状态的关系
return <div>{user.name}</div>;
这种声明式的模式让代码更可读、可维护,也更容易测试。
3. 跨平台能力:一次描述,多端渲染
虚拟 DOM 只负责描述 UI 结构,不绑定具体的渲染目标。在 Web 中它被渲染为 DOM;在移动端(React Native)它被渲染为原生控件;在测试中它可以直接输出为字符串。这种抽离让“学一套,写多处”成为可能。
4. 状态同步:避免直接修改 DOM 导致的“状态失控”
当你手动操作 DOM 时,用户的交互、网络响应、定时器等可能在同一时刻修改同一个 DOM 节点,状态分散在 DOM 各处,调试和追踪都异常困难。虚拟 DOM 配合单向数据流(如 Vue 的响应式、React 的 state),让数据变化可预测,UI 成为状态的“纯函数”。
13.6.5 没有银弹:虚拟 DOM 的成本
虚拟 DOM 并不是零开销。它需要在内存中维护虚拟节点、执行 diff 算法,这些计算虽然比 DOM 操作快,但也会消耗 CPU。对于非常简单的交互(比如只更新一个计数器),直接操作 DOM 可能的代码量和性能开销都比引入一整套虚拟 DOM 框架要小。
因此,现代框架都在不断优化 diff 算法(如 Vue 3 的静态标记、React 的 Fiber 架构),力求把虚拟 DOM 的开销降到最低。在实际项目中,选择直接操作 DOM 还是使用虚拟 DOM,取决于应用的规模和复杂度:一旦交互复杂度上升到一定程度,虚拟 DOM 带来的工程收益会远大于其运行时成本。
13.6.6 本质:用 JavaScript 计算换取 DOM 操作
归根结底,虚拟 DOM 的本质是一种权衡:用更便宜的 JavaScript 计算,换取昂贵的 DOM 操作。 浏览器里的 DOM 是慢的,但 JavaScript 引擎的运行速度很快。在 JS 内存中对比两棵树并计算出最小变更,比你自己硬着头皮去分析“哪些 DOM 节点需要动”要高效、可靠得多。
理解这一点后,你就不再仅仅把虚拟 DOM 当成框架的一个黑盒特性,而是能看清它在浏览器渲染性能优化中的位置——与文档碎片、重排重绘优化等手段一脉相承,只不过它将优化的高度提升到了整个应用状态管理的层面。