什么是虚拟 DOM?它相比直接操作真实 DOM 有哪些优势,又存在哪些开销?
一句话结论
虚拟 DOM 是一棵存在于内存中的轻量级 JavaScript 对象树,它模拟真实 DOM 的结构;框架在数据变化时先对比新旧虚拟 DOM 树(Diff 算法),算出最小差异后再批量更新真实 DOM。它的核心价值不在于"比直接操作 DOM 更快"(这是个常见误区),而在于把命令式的手动 DOM 操作抽象为声明式渲染 + 批量优化更新,代价是多了一层内存和计算开销。
一、什么是虚拟 DOM
虚拟 DOM(Virtual DOM,简称 VDOM)是用纯 JavaScript 对象描述真实 DOM 结构的一种抽象表示。它不依赖浏览器 API,仅存在于内存中。
一个真实 DOM 节点与对应的虚拟 DOM 对比:
▼html复制代码<!-- 真实 DOM --> <div class="container"> <h1>Hello</h1> <button onclick="handle()">点击</button> </div>
▼javascript复制代码// 对应的虚拟 DOM(一个 JS 对象树) { type: 'div', props: { className: 'container' }, children: [ { type: 'h1', props: {}, children: 'Hello' }, { type: 'button', props: { onClick: handle }, children: '点击' } ] }
真实 DOM 节点非常"重"——一个普通 <div> 在浏览器中可能包含 200+ 个属性(如 style、classList、innerHTML、事件监听器等),创建和操作代价高。虚拟 DOM 是"轻量"的——一个对象只有 type、props、children 几个字段,创建成本远低于真实 DOM 节点。
为什么真实 DOM 慢? 每次修改真实 DOM 都可能触发浏览器的重排(Reflow)和重绘(Repaint)。重排会重新计算元素的几何属性和布局,重绘会重绘受影响区域,二者都是昂贵的同步操作。频繁的 DOM 操作会导致页面卡顿。
二、虚拟 DOM 的工作流程
▼text复制代码状态变化 │ ▼ ① 生成新的虚拟 DOM 树(描述 UI 最新状态) │ ▼ ② Diff 算法:对比新旧虚拟 DOM 树,找出差异(Patch) │ ▼ ③ 将差异批量应用到真实 DOM(最小化 DOM 操作) │ ▼ ④ 浏览器仅执行必要的重排/重绘
Diff 算法的三个优化策略
Diff 算法负责对比新旧 VDOM 树,计算出最小变更集。直接对比两棵树的差异,朴素算法复杂度为 $O(n^3)$,工程上不可用。React/Vue 的 Diff 采用了三个假设将其降至 $O(n)$:
| 策略 | 内容 | 原因 |
|---|---|---|
| 同层比较 | 只对比同一层级的节点,不跨层级移动 | 跨层级移动节点在实际 UI 中极少发生 |
| 类型不同直接替换 | 节点类型(标签名)不同 → 删除旧节点及其子树,创建新节点 | 不同类型的组件大概率产出不同结构 |
key 优化列表 | 列表节点用唯一 key 标识 → 复用与移动而非全部重建 | 解决列表插入/删除时的节点错位问题 |
▼javascript复制代码// 有 key:Diff 能精确识别"移动",只移动 DOM 节点 <ul> <li key="a">Apple</li> <li key="b">Banana</li> <li key="c">Cherry</li> {/* 在头部插入,b、c 整体下移即可 */} </ul> // 无 key:Diff 逐个对比,发现每个 <li> 的文本都"变了" // → 逐个更新文本内容(3 次文本替换,而非 1 次节点移动)
三、虚拟 DOM 相比直接操作真实 DOM 的优势
| 优势 | 说明 |
|---|---|
| 批量更新,减少重排/重绘 | 直接操作 DOM 时,每次 appendChild、修改 style 都可能立即触发重排。虚拟 DOM 将多次状态变化收集后,Diff 一次性算出最小变更集再提交,避免中间过程的无效渲染 |
| 声明式编程 | 开发者只需描述"数据→UI"的映射关系,框架自动算出需要操作哪些 DOM,消除手动维护 DOM 同步的心智负担 |
| 跨平台 | 虚拟 DOM 是纯 JS 对象,不依赖浏览器。同一套组件逻辑可渲染到浏览器 DOM、移动端原生组件(React Native)、SSR(字符串 HTML)、Canvas 等 |
| 可预测的更新 | Diff 算法保证每次更新都是"从状态到 UI 的确定映射",避免手动操作时遗漏更新分支导致的 UI 与数据不同步 |
| 调试友好 | 虚拟 DOM 是可序列化的 JS 对象,可快照、可时间旅行调试(如 Redux DevTools) |
"批量更新"的性能收益示例
▼javascript复制代码// 直接操作真实 DOM:3 次重排 document.getElementById('a').textContent = '1'; // 重排 #1 document.getElementById('b').textContent = '2'; // 重排 #2 document.getElementById('c').textContent = '3'; // 重排 #3 // 虚拟 DOM:1 次重排 // 框架先在内存中计算 a/b/c 全部变化 → 一次性提交 → 浏览器只重排 1 次
四、虚拟 DOM 的开销
虚拟 DOM 并非免费午餐,它引入了额外的计算和内存成本:
开销 1:内存占用
框架需要同时维护虚拟 DOM 树 + 真实 DOM 树两份数据结构。每个组件的渲染输出都会生成对应的 VDOM 对象树,大型应用的 VDOM 可能包含数万节点,内存占用不可忽视。
开销 2:Diff 计算成本
每次状态变化都要执行 Diff 算法遍历新旧 VDOM 树。虽然通过三大假设将复杂度降至 $O(n)$,但 $n$ 很大时仍有可观开销——尤其在频繁更新或超长列表场景下。
$$ \text{总成本} = \underbrace{O(n){\text{Diff 计算}}}{\text{JS 层}} + \underbrace{O(k){\text{真实 DOM 操作}}}{\text{浏览器层}} $$
当 $k$(实际变化的节点数)很小、$n$(VDOM 树节点总数)很大时,Diff 计算开销 $O(n)$ 可能超过直接操作 DOM 的开销 $O(k)$。这就是为什么 Svelte 等编译型框架选择在编译阶段直接生成精确更新代码,运行时无 Diff 开销。
开销 3:初始渲染开销
首次渲染时,框架需要先构建完整的虚拟 DOM 树,再一次性转为真实 DOM。相比直接用 innerHTML 写死 HTML,多了一次"JS 对象树构建→真实 DOM 创建"的转换过程。
开销 4:并非总是比直接操作 DOM 更快
这是一个重要误区。Vue 官方文档和多个技术分析都明确指出:虚拟 DOM 不保证比精心编写的命令式 DOM 操作更快。对于单个元素的简单更新,直接 element.textContent = newValue 比走一遍 VDOM 创建→Diff→提交的完整流程要快得多。虚拟 DOM 的优势在于大规模、批量、声明式场景下的开发效率与性能平衡,而非极端性能。
五、虚拟 DOM vs 真实 DOM 全景对比
| 维度 | 直接操作真实 DOM | 虚拟 DOM |
|---|---|---|
| 编程模型 | 命令式:手动 createElement、appendChild | 声明式:描述 UI 与数据的映射 |
| 更新方式 | 每次操作可能立即触发重排/重绘 | 收集变化→Diff→批量提交,减少重排次数 |
| 跨平台 | 仅限浏览器 DOM | 纯 JS 对象,可渲染到多端 |
| 内存开销 | 仅维护真实 DOM 树 | 额外维护一整棵 VDOM 树 |
| 计算开销 | 无中间计算,直接操作 | 每次 Diff 需 $O(n)$ 遍历 |
| 简单更新性能 | ✅ 最快(直接改 DOM) | ❌ 更慢(多走一层 Diff) |
| 大规模批量更新 | ❌ 易产生多次重排 | ✅ 批量合并,重排次数最少 |
| 开发效率 | ❌ 手动维护,易出错 | ✅ 声明式,自动同步 |
| 代表性实现 | 原生 JS、jQuery | React、Vue、Inferno |
六、技术演进脉络
虚拟 DOM 并非终点,前端渲染技术仍在演进:
▼text复制代码2013 React 引入虚拟 DOM,将"声明式渲染 + Diff"范式推向主流 │ 2016 Inferno 等项目探索更快的 VDOM 实现(更优 Diff 算法) │ 2019 Vue 2→3 优化:编译器静态标记(PatchFlag),Diff 时跳过静态子树 │ 2019 Svelte 走另一条路:编译时直接生成精确 DOM 更新代码,运行时零 VDOM 开销 │ 2021 SolidJS:细粒度响应式(Signal),无 VDOM,数据变→直接改对应 DOM │ 2023 React 18 并发渲染(Fiber 架构):Diff 可中断/恢复,优先级调度 │ 2025+ 框架持续融合:编译优化(Qwik 延迟加载、Vue Vapor Mode 去运行时)
Svelte 和 SolidJS 的存在恰恰反证了:虚拟 DOM 不是最优解,它是一种工程权衡——用可接受的运行时开销,换取声明式开发体验和跨平台能力。
记忆口诀
虚拟 DOM 是"草稿纸"——你先在草稿纸上写写画画(在内存中构建 VDOM 树),算好最终改动方案(Diff),再一次性誊抄到正式答卷上(真实 DOM),避免反复涂改触发重排。草稿纸要额外花钱(内存开销),誊抄前要花时间比对(Diff 计算),但总比在正式答卷上来回涂改高效——除非你只改一个字,那直接在答卷上改反而最快。
参考来源
