什么是虚拟 DOM
虚拟 DOM(Virtual DOM)是用 JavaScript 对象来描述真实 DOM 结构的一棵树,存在于内存中,是真实 DOM 的轻量级副本。
内存中的实际结构
React Element
虚拟 DOM 节点是一个普通的 JavaScript 对象(Plain Object),React 内部称它为 React Element,由 React.createElement() 创建。
// 你写的 JSX
const el = <div className="box"><h1>Hello</h1></div>
// Babel 编译后实际执行的
const el = React.createElement(
'div',
{ className: 'box' },
React.createElement('h1', null, 'Hello')
)
// 返回的对象结构
{
$$typeof: Symbol(react.element), // 防 XSS 的安全标记
type: 'div',
key: null,
ref: null,
props: {
className: 'box',
children: {
$$typeof: Symbol(react.element),
type: 'h1',
key: null,
ref: null,
props: { children: 'Hello' },
_owner: null,
}
},
_owner: null,
}
树状结构靠 props.children 的引用嵌套形成,多个子节点时 children 是一个数组:
div (React Element)
└── props.children: [
h1 (React Element)
└── props.children: "Hello"
p (React Element)
└── props.children: "World"
]
React Element 只是"描述长什么样"的不可变快照,每次渲染都会重新创建,不会长期驻留内存。
为什么需要虚拟 DOM
直接操作真实 DOM 代价昂贵,每次访问或修改 DOM,浏览器都要触发完整的渲染流水线:
样式计算 → 布局(Layout)→ 绘制(Paint)→ 合成(Composite)
频繁操作 DOM 会导致多次"重排(reflow)“和"重绘(repaint)",性能开销很大。
虚拟 DOM 的核心思路:先在内存中计算出最终状态,再一次性以最小代价更新真实 DOM。
工作流程
状态变化(setState)
↓
生成新的虚拟 DOM 树
↓
与旧的虚拟 DOM 树做 Diff(差异比较)
↓
找出最小变更集(Patch)
↓
一次性更新真实 DOM
Diff 算法
朴素的树 Diff 算法复杂度是 O(n³),React 通过三个启发式假设将其降低到 O(n):
1. 跨层级移动不考虑
不同层级的节点直接删除重建,不做跨层移动匹配。
2. 同类型节点才递归比较
节点类型不同(如 div → span),直接销毁整棵子树并重建,不尝试复用。
3. key 优化列表 Diff
列表渲染时,key 帮助 React 识别节点身份,区分"移动"和"新增/删除”:
// 没有 key:React 按位置比较,插入一项可能导致后续全部重建
<ul>
<li>Apple</li>
<li>Banana</li>
</ul>
// 有 key:React 能精确追踪每个节点,只做必要的更新
<ul>
<li key="1">Apple</li>
<li key="2">Banana</li>
</ul>
常见错误:用数组 index 做 key,在列表头部插入时 index 全部变化,Diff 结果和没有 key 一样差。应使用稳定唯一的业务 ID。
虚拟 DOM 一定比直接操作 DOM 快吗
不一定,这是常见误区。
| 场景 | 结论 |
|---|---|
| 频繁、复杂的状态更新 | 虚拟 DOM 更高效(批量计算后统一更新) |
| 简单的单次 DOM 操作 | 直接操作真实 DOM 反而更快(省去 Diff 开销) |
虚拟 DOM 真正的价值在于:
- 开发体验:声明式描述 UI,不用手动管理 DOM 操作顺序
- 跨平台:虚拟 DOM 是平台无关的抽象层,React Native 用同一套机制渲染原生组件
Fiber 架构(React 16+)
React Element 只是不可变的描述快照,真正在内存里长期存活、用于调度和渲染的是 Fiber 节点。每个组件或 DOM 节点都对应一个 Fiber 对象:
{
// 组件信息
type: 'div', // 或函数组件、类组件
key: null,
// 链表指针(不是数组!)
return: FiberNode, // 父节点
child: FiberNode, // 第一个子节点
sibling: FiberNode, // 右边的兄弟节点
// 状态和副作用
memoizedState: ..., // hooks 链表 / 类组件 state
memoizedProps: {...},
pendingProps: {...},
flags: 0b000010, // 标记需要插入/更新/删除
lanes: ..., // 优先级
// 双缓冲
alternate: FiberNode, // 指向另一棵树的对应节点
}
Fiber 树用链表而非数组表示子节点:child 只指向第一个子节点,兄弟节点通过 sibling 串联。这样遍历时可以随时中断并记住当前位置,下次继续——这是并发模式可中断渲染的底层基础。
双缓冲机制
React 同时维护两棵 Fiber 树:
current树:当前屏幕上显示的版本workInProgress树:正在后台计算的新版本
两棵树的节点通过 alternate 互相引用,计算完成后直接切换指针,类似 GPU 渲染里的双缓冲,避免中间状态被用户看到。
current tree ←── alternate ──→ workInProgress tree
↑ ↓
屏幕显示 后台计算中
Fiber 让渲染工作可以拆分成小单元,支持中断和恢复,为 React 18 的并发特性奠定基础。
React 18 并发模式
React 18 在 Fiber 基础上引入 Concurrent Mode(并发模式):
- 渲染可以中断、暂停、恢复,不再是同步阻塞的
- 高优先级更新(如用户点击)可以打断低优先级更新(如后台数据渲染)
- 相关 API:
useTransition、useDeferredValue、startTransition
const [isPending, startTransition] = useTransition();
// 低优先级更新,不阻塞用户交互
startTransition(() => {
setSearchResults(computeHeavyFilter(input));
});
小结
| 概念 | 要点 |
|---|---|
| 虚拟 DOM | JS 对象描述的 DOM 树,存在于内存 |
| React Element | React.createElement() 返回的不可变 Plain Object,每次渲染重新创建 |
$$typeof |
Symbol 标记,防止 JSON 注入伪造 React Element |
| Diff 算法 | O(n) 复杂度,三个启发式规则 |
| key | 帮助识别列表节点身份,应使用稳定业务 ID |
| Fiber | 用链表(child/sibling/return)表示树,可中断,支持优先级调度 |
| 双缓冲 | current 树 + workInProgress 树,alternate 互指,渲染完切换指针 |
| 并发模式 | 高优先级任务可打断低优先级渲染 |