
一句话:虚拟 DOM 是「用普通 JS 对象描述 UI 结构」的一层抽象;React 通过比较新旧两棵对象树来算出最小 DOM 操作——它的价值是「可预测的更新模型 + 跨平台能力」,而不是「比直接操作 DOM 更快」。
一、虚拟 DOM 是什么
// 你写的 JSX
const ui = (
<ul className="list">
<li>第一项</li>
<li>第二项</li>
</ul>
);
// 编译后(简化)——就是普通的对象
const ui = {
type: 'ul',
props: {
className: 'list',
children: [
{ type: 'li', props: { children: '第一项' } },
{ type: 'li', props: { children: '第二项' } },
],
},
};
虚拟 DOM(Virtual DOM)的准确定义:
一棵用 JavaScript 对象表示的「UI 描述树」。
它不是 DOM,也没有浏览器资源——只是数据。
React 的工作流程:
① 组件返回元素树(虚拟 DOM)
② React 把新的元素树与上一次的对比(diff / 协调)
③ 算出「需要哪些 DOM 操作」
④ 批量把这些操作应用到真实 DOM(commit)
三个容易混淆的概念:
① React Element(元素)
不可变的对象,描述「某个时刻的 UI 长什么样」
{ $$typeof, type, props, key, ref }
② Fiber(Fiber 节点)
一个「可持久化的工作单元」,除了元素信息外还包含:
父子兄弟指针、备用节点(alternate)、优先级(lanes)、副作用标记(flags)
它是 React 内部用来「可中断地工作」的数据结构
③ 真实 DOM
浏览器对象,有布局与内存开销
关系:Element 是「说明书」,Fiber 是「施工计划」,DOM 是「建成的房子」
二、为什么需要虚拟 DOM(三个真实理由)
1) 声明式编程的前提
// 命令式:你要自己算「该改什么」
function updateUI(count) {
const el = document.getElementById('count');
if (count > 0) {
el.textContent = String(count);
el.classList.add('positive');
} else {
el.classList.remove('positive');
}
}
// 声明式:你只说「应该长什么样」,React 负责算差异
function Counter({ count }) {
return <span className={count > 0 ? 'positive' : ''}>{count}</span>;
}
价值:把「状态 → UI 的映射」写成纯函数,**不需要维护「从状态到 DOM 操作」的命令式代码**。
这是虚拟 DOM 最核心的价值——「可预测的更新模型」。
(Vue 的模板、Svelte 的编译时方案也是在解决这个问题,只是路径不同。)
2) 批量更新与减少不必要的 DOM 操作
// 一次渲染中可能有大量状态变化,但只有少数会影响 DOM
function Form() {
const [a, setA] = useState('');
const [b, setB] = useState('');
// 输入 a 时,b 对应的 DOM 完全不需要变
return (
<>
<input value={a} onChange={(e) => setA(e.target.value)} />
<input value={b} onChange={(e) => setB(e.target.value)} />
</>
);
}
虚拟 DOM 让 React 可以在「计算完所有差异后一次性应用」:
① 把更新积累起来(批处理)
② 计算最小差异集合
③ 统一应用到 DOM(减少 layout thrashing)
⚠️ 注意:这里的收益是「减少开发者手写命令式代码的出错概率」与「集中调度更新」,
而不是「算法层面比手写 DOM 操作更快」。
3) 跨平台能力
同一棵「虚拟 DOM」可以渲染到不同目标:
· react-dom → 浏览器 DOM
· react-native → 原生 iOS/Android 组件
· react-three-fiber → WebGL
· react-pdf → PDF
· ink → 终端
· react-canvas → Canvas
实现方式就是「替换渲染器」——组件的写法和状态管理完全复用。
这是 JSX 作为「UI 描述语言」带来的最大红利。
三、「虚拟 DOM 比原生 DOM 快」是误解
准确的说法是:
❌ 「虚拟 DOM 比直接操作 DOM 快」
→ 恰恰相反:虚拟 DOM 是在「真实的 DOM 操作之上」又加了一层计算。
如果你手写「命令式的最小 DOM 操作」,一定会比「先 diff 再操作」快。
✅ 「虚拟 DOM 在『不手动优化』的前提下,避免了很多常见的性能陷阱」
→ 它让「整体性能的下限」变高:
· 不用关心「该更新哪个节点」(避免漏更新、重复更新)
· 天然批处理(避免频繁触发同步布局)
· 容易做「跳过不需要更新的子树」的优化(memo)
✅ 「虚拟 DOM 的代价换来了:声明式、可预测、跨平台」
→ 这是划算的交易,但不是「绝对更快」。
| 说法 | 是否成立 |
|---|---|
| 虚拟 DOM 一定比手写 DOM 操作快 | ❌ 错误 |
| 虚拟 DOM 让「不优化」时的性能不至于太差 | ✅ |
| 虚拟 DOM 提供了声明式编程与跨平台能力 | ✅ 主要价值 |
| Svelte 这种「编译时方案」可以比虚拟 DOM 快 | ✅(因为它把 diff 生成为直接 DOM 操作) |
为什么 Svelte / Solid 能更快?
它们在【编译时】就知道「哪个状态影响哪个 DOM 节点」,
于是直接生成「更新那个节点的代码」,跳过了运行时的 diff。
React 因为「每次渲染都重新执行组件函数」的模型,
必须在运行时比较(所以又有了 React Compiler 来在编译时做记忆化)。
四、虚拟 DOM 的代价
① 内存开销
每个元素/组件都要创建一个对象(Element + Fiber),大树会有可观的内存占用
② 计算开销
每次渲染都要「遍历 + 比较」整棵(未被 memo 跳过的)子树
③ 首次渲染
必须「创建对象 → 构建 Fiber 树 → 生成 DOM」,比直接写 HTML 慢
④ 心智模型
开发者需要理解「渲染是重新执行函数」「引用稳定」等概念才能优化
React 在这些代价上做的优化:
① Fiber + 时间切片 → 长渲染不阻塞主线程(见 4.2)
② memo / useMemo → 跳过不需要更新的子树
③ 批处理 → 合并多次更新为一次渲染
④ React Compiler → 编译时自动插入记忆化,减少运行时比较
⑤ 编译时标记(如 static flags)→ 复用「不会变的子树」
五、React Element 的结构回顾
// 一个 Element 的真实结构(简化)
{
$$typeof: Symbol(react.element), // 防伪造(JSON 无法构造 Symbol)
type: 'ul', // 字符串(宿主组件)或函数/类(自定义组件)
key: null, // 列表中的身份标识
ref: null,
props: {
className: 'list',
children: [/* 子元素 */],
},
_owner: null, // 调试用(谁创建了它)
}
type 的值 |
含义 | 渲染方式 |
|---|---|---|
字符串 'div' |
宿主组件(Host Component) | 创建/更新真实 DOM 元素 |
| 函数 | 函数组件 | 调用函数得到元素树 |
| 类 | 类组件 | 实例化并调用 render |
Fragment / Symbol |
特殊类型 | 不产生 DOM 或特殊处理 |
Symbol(react.portal) |
Portal | 渲染到指定容器 |
六、常见坑
// ① 以为「虚拟 DOM 就是 DOM 的副本」
// → 它只是「描述结构的数据」,不含样式、布局、事件
// ② 以为「虚拟 DOM 让操作 DOM 变快了」
// → 它是额外的抽象层;快是因为「批量 + 跳过」
// ③ 在渲染中做副作用(以为「渲染只是生成对象」很便宜)
function Bad() {
fetch('/api'); // ❌ 渲染函数应该纯
return <div />;
}
// ④ 直接改 state 后期待 UI 更新
user.name = 'x'; // ❌ 引用没变 → diff 认为「没变化」
// ⑤ 用 index 作 key(破坏了 diff 正确复用节点的基础)
// ⑥ 认为「元素是不可变的,所以不能更新 UI」
// → 更新是「创建新元素树」,由 React 比较(diff)后应用到 DOM
// ⑦ 手动操作 DOM 与 React 打架
ref.current.innerHTML = '<div>...</div>'; // ⚠️ React 不知道你改了它
// → 下次渲染可能被覆盖,或引发 hydration mismatch
// ⑧ 认为「跨平台能力是虚拟 DOM 的副产品,不用特意设计」
// → 它要求渲染器与「宿主无关」的抽象(React 用 host config 实现)
// ⑨ 认为「用了虚拟 DOM 就不需要关心 DOM 性能了」
// → 大量 DOM 节点、频繁布局抖动依然会卡(见 05 章)
// ⑩ 同时使用多个 React 渲染器版本(会报 Invalid hook call)
七、面试延伸
- 「什么是虚拟 DOM?」
虚拟 DOM 是「用 JavaScript 对象描述 UI 结构的树」——它不是 DOM、也没有浏览器资源,只是数据。React 让组件返回这样一棵树,然后把它与上一次的树做差异比较(diff/协调),算出「需要哪些真实的 DOM 操作」,再批量应用到浏览器。它的核心价值是「让 UI 成为状态的纯函数映射」,实现声明式编程。 -
「虚拟 DOM 比直接操作 DOM 快吗?」
不是。虚拟 DOM 是在真实 DOM 操作之上又加了一层计算(创建对象、diff、遍历),所以「手写最优的命令式 DOM 操作」一定比它快。它的真实价值是:① 声明式——不需要手写「该改哪个节点」的命令式代码;② 减少低级错误——不会漏更新或重复更新;③ 批处理与跳过——把多次更新合并、跳过不需要更新的子树;④ 跨平台。用一句话概括是「用一点性能换取可预测性与开发效率」。 -
「为什么 React 需要虚拟 DOM?」
三个理由:① 声明式的前提——要有「新旧 UI 描述」才能比较,从而让开发者只描述结果;② 跨平台——同一棵虚拟 DOM 可以渲染到 DOM、Native、Canvas、PDF 等(替换渲染器即可);③ 统一调度——有了中间层,React 才能做批处理、优先级调度、可中断渲染(这也是 Fiber 的前提)。 -
「React Element、Fiber、真实 DOM 有什么区别?」
Element:不可变的对象,描述「某一时刻 UI 长什么样」(含type/props/key/$$typeof);Fiber:React 内部的「工作单元」,在 Element 信息之外还带「父子兄弟指针、alternate(另一棵树的对应节点)、lanes(优先级)、flags(副作用标记)」,它是「可中断渲染」的数据结构;真实 DOM:浏览器的对象,有布局与内存开销。打个比方:Element 是说明书,Fiber 是施工计划,DOM 是建成的房子。 -
「虚拟 DOM 的代价是什么?」
四个:① 内存——每个元素/组件节点都要创建对象;② 计算——每次渲染都要遍历比较(未被memo跳过的子树);③ 首次渲染更慢——需要「创建对象 → 建 Fiber 树 → 生成 DOM」,比直接输出 HTML 慢;④ 心智成本——开发者要理解「渲染即重新执行函数」「引用稳定」等概念。React 用 Fiber 时间切片、memo、批处理、React Compiler 等手段来抵消这些代价。 -
「Svelte / Solid 为什么可能更快?」
因为它们走的是「编译时方案」:在编译阶段就知道「哪个状态变量影响哪一处 DOM」,于是直接把更新逻辑编译成「操作那个具体节点的代码」,运行时不需要 diff。React 选择的是「运行时方案」——因为「每次渲染都重新执行组件函数」的模型更灵活(组件是普通 JS),但代价就是运行时要做比较与记忆化。两者是不同的取舍,不是绝对的优劣。 -
「什么情况下不该用虚拟 DOM?」
当「更新模式极其固定且性能极端敏感」时——比如静态内容(直接输出 HTML 更快)、极高频的动画/可视化(用 Canvas/WebGL 直接绘制更好)、需要处理上万 DOM 节点的场景(虚拟滚动或 Canvas 更合适)。React 生态在这些场景里通常的做法是「绕开虚拟 DOM」:用React.memo隔离、用ref直接操作、或把绘制交给 Canvas。 -
「虚拟 DOM 和模板编译(Vue 的模板)有什么区别?」
两者都在解决「声明式 UI + 高效更新」,区别在中间表示与优化时机:① React 用 JSX + 运行时——JSX 编译为函数调用,结构信息在运行时由 React 通过 diff 得出;② Vue 用模板 + 编译时优化——模板在编译时能分析出「静态节点、动态绑定、依赖关系」,生成带优化提示的渲染函数(如PatchFlags),从而在运行时跳过大部分比较(Vue 3 还有编译时生成的Block Tree)。所以 Vue 在「模板场景」下往往有更少的运行时开销,而 React 的表达力(JS 的完整能力)更强。
一句话速记
虚拟 DOM 是「用 JS 对象描述 UI 的树」(Element 是说明书、Fiber 是施工计划、DOM 是房子);它的价值是「声明式 + 可预测的更新模型 + 批处理 + 跨平台」,而不是「比原生 DOM 快」(它是额外一层计算,手写最优 DOM 操作一定更快);代价是内存 + 遍历比较 + 首屏更慢,React 用 Fiber 时间切片 / memo / 批处理 / Compiler 抵消;Element 是不可变对象(
$$typeof用 Symbol 防伪造),更新 = 创建新树 + diff + 应用到 DOM。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!