mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2335 字
7 分钟
为什么 React 使用虚拟 DOM
2024-02-09

React 在 2013 年发布时,虚拟 DOM(Virtual DOM)是其最具创新性的设计之一。这个设计让”数据驱动视图”成为前端开发的主流范式。但虚拟 DOM 真的比直接操作 DOM 更快吗?答案并非简单的”是”或”否”。理解虚拟 DOM 的设计动机,需要回到 2013 年的前端开发背景,理解它解决了什么问题,以及它带来的权衡。

一、直接操作 DOM 的性能问题#

1.1 DOM 操作的真实成本#

很多人认为 DOM 操作慢是因为”DOM 是用 C++ 实现的,应该很快”。问题不在于 DOM 本身的实现,而在于浏览器渲染管线的连锁反应。

flowchart TD subgraph 浏览器渲染管线 JS[JavaScript 执行] --> STYLE[样式计算] STYLE --> LAYOUT[布局 Layout] LAYOUT --> PAINT[绘制 Paint] PAINT --> COMPOSITE[合成 Composite] end subgraph DOM 操作触发 D1[修改 DOM 结构] D2[修改元素样式] D3[读取布局属性] end D1 --> STYLE D2 --> STYLE D3 --> LAYOUT style LAYOUT fill:#f96,stroke:#333 style PAINT fill:#f96,stroke:#333

一次简单的 DOM 操作可能触发完整的渲染管线:

// 这段代码会导致多少次重排?
const container = document.getElementById("container");
for (let i = 0; i < 100; i++) {
const div = document.createElement("div");
div.textContent = `Item ${i}`;
div.style.height = "50px"; // 触发样式计算
container.appendChild(div); // 触发布局
console.log(container.offsetHeight); // 强制同步布局!
}

1.2 强制同步布局(Layout Thrashing)#

最严重的性能问题来自强制同步布局(Forced Synchronous Layout):

sequenceDiagram participant JS as JavaScript participant DOM as DOM participant Layout as 布局引擎 Note over JS,Layout: 错误方式:读写交替 JS->>DOM: 修改元素宽度 JS->>Layout: 读取 offsetWidth(强制布局) Layout-->>JS: 返回计算值 JS->>DOM: 修改元素宽度 JS->>Layout: 读取 offsetWidth(再次强制布局) Layout-->>JS: 返回计算值 Note over Layout: 每次读取都触发完整布局计算! Note over JS,Layout: 正确方式:批量读写 JS->>DOM: 修改元素宽度 x N JS->>DOM: 修改元素宽度 x N JS->>Layout: 读取 offsetWidth x N(只需一次布局)
// 性能灾难:读写交替
const elements = document.querySelectorAll(".item");
elements.forEach(el => {
el.style.width = "100px"; // 写
console.log(el.offsetWidth); // 读 - 强制布局!
});
// 优化方式:批量读写分离
const elements = document.querySelectorAll(".item");
const widths = [];
// 先读
elements.forEach(el => widths.push(el.offsetWidth));
// 再写
elements.forEach((el, i) => {
el.style.width = widths[i] + 10 + "px";
});

1.3 2013 年的前端困境#

在 React 发布之前,前端开发面临的核心问题:

flowchart TB subgraph 命令式编程的困境 A[状态变化] --> B{哪些 DOM 需要更新?} B --> C[手动追踪所有变化] C --> D[代码复杂度爆炸] D --> E[Bug 频发] E --> F[难以维护] end subgraph 具体问题 G[状态分散在多处] H[更新逻辑与业务逻辑耦合] I[难以预测视图状态] J[性能优化需要专业知识] end A --> G B --> H C --> I D --> J style D fill:#f66,stroke:#333 style F fill:#f66,stroke:#333

当时的主流框架(jQuery、Backbone、Angular 1.x)都采用命令式编程:

// jQuery 时代的典型代码
function updateTodoList(todos) {
// 手动管理 DOM 更新
const $list = $("#todo-list");
$list.empty(); // 清空
todos.forEach(todo => {
const $item = $("<li>").text(todo.text);
if (todo.completed) {
$item.addClass("completed");
}
$item.appendTo($list);
});
// 更新计数器
const completedCount = todos.filter(t => t.completed).length;
$("#completed-count").text(completedCount);
// 更新状态文本
if (todos.length === 0) {
$("#empty-state").show();
} else {
$("#empty-state").hide();
}
}

1.4 DOM 操作的性能数据#

直接 DOM 操作的实际开销:

操作类型时间复杂度实际耗时(1000 次)
createElementO(1)~1ms
appendChildO(n)~10ms
innerHTML = ...O(n)~5ms
element.style.xxx = ...O(1)~0.1ms
读取 offsetWidth/heightO(n)~50ms(强制布局)
修改 class 触发重排O(n)~20ms
xychart-beta title "DOM 操作性能对比(1000 次操作,毫秒)" x-axis ["createElement", "appendChild", "innerHTML", "修改style", "读取offsetWidth"] y-axis "时间(ms)" 0 --> 60 bar [1, 10, 5, 0.1, 50]

二、虚拟 DOM 的设计理念#

2.1 核心思想:声明式 UI#

虚拟 DOM 的核心不是”更快”,而是让声明式 UI 成为可能

flowchart LR subgraph 命令式编程 A1[状态变化] --> B1[手动计算差异] B1 --> C1[执行 DOM 操作] C1 --> D1[更新完成] end subgraph 声明式编程(React) A2[状态变化] --> B2[描述目标 UI] B2 --> C2[React 计算差异] C2 --> D2[批量更新 DOM] end style B1 fill:#f96,stroke:#333 style C1 fill:#f96,stroke:#333 style C2 fill:#9f6,stroke:#333 style D2 fill:#9f6,stroke:#333
// React 的声明式写法
function TodoList({ todos }) {
const completedCount = todos.filter(t => t.completed).length;
return (
<div>
<ul>
{todos.map(todo => (
<li key={todo.id} className={todo.completed ? "completed" : ""}>
{todo.text}
</li>
))}
</ul>
<p>已完成: {completedCount}</p>
{todos.length === 0 && <p>暂无待办事项</p>}
</div>
);
}
// 只需要告诉 React "渲染成什么样"
// 不需要关心 "怎么更新"

2.2 虚拟 DOM 的本质#

虚拟 DOM 是真实 DOM 的 JavaScript 对象表示:

flowchart TB subgraph JSX 代码 JSX["&lt;div className='container'&gt;<br/> &lt;span&gt;Hello&lt;/span&gt;<br/>&lt;/div&gt;"] end subgraph 虚拟 DOM 树 VDOM["{<br/> type: 'div',<br/> props: { className: 'container' },<br/> children: [<br/> { type: 'span', children: 'Hello' }<br/> ]<br/>}"] end subgraph 真实 DOM 树 RDOM["&lt;div class='container'&gt;<br/> &lt;span&gt;Hello&lt;/span&gt;<br/>&lt;/div&gt;"] end JSX -->|Babel 转译| VDOM VDOM -->|ReactDOM.render| RDOM style VDOM fill:#9cf,stroke:#333
// JSX 转译后的真实代码
const element = React.createElement(
"div",
{ className: "container" },
React.createElement("span", null, "Hello")
);
// 虚拟 DOM 对象结构
{
$$typeof: Symbol(react.element),
type: "div",
key: null,
ref: null,
props: {
className: "container",
children: {
type: "span",
props: {
children: "Hello"
}
}
}
}

2.3 为什么用 JavaScript 对象表示?#

特性JavaScript 对象真实 DOM
创建成本极低较高
内存占用大(包含很多属性)
可比较性容易实现 diff需要遍历 DOM 树
跨平台与平台无关浏览器特有
可序列化JSON.stringify需要 XML 序列化

2.4 虚拟 DOM 的价值主张#

mindmap root((虚拟 DOM 价值)) 开发体验 声明式编程 组件化思维 关注点分离 性能保障 批量更新 最小化 DOM 操作 避免强制同步布局 跨平台能力 React Native React ART 服务端渲染 生态建设 开发者工具 测试工具 状态管理

三、Diff 算法原理#

3.1 Diff 算法的核心假设#

React 的 Diff 算法基于三个核心假设,将 O(n³) 复杂度降低到 O(n):

flowchart TB A[理论最小编辑距离<br/>O n³ 算法] --> B{React 的假设} B --> C[假设 1: 同层比较<br/>不同层级节点不比较] B --> D[假设 2: 类型不同则重建<br/>不同类型节点直接替换] B --> E[假设 3: key 标识节点<br/>通过 key 追踪节点身份] C --> F[复杂度降至 O n] D --> F E --> F style A fill:#f96,stroke:#333 style F fill:#9f6,stroke:#333

3.2 同层比较策略#

React 只比较同一层级的节点,不跨层级比较:

flowchart TB subgraph 旧树 OLD_A[A] OLD_B[B] OLD_C[C] OLD_D[D] OLD_E[E] end subgraph 新树 NEW_A[A'] NEW_B[B'] NEW_C[C'] NEW_F[F] end OLD_A -.->|比较| NEW_A OLD_B -.->|比较| NEW_B OLD_C -.->|比较| NEW_C OLD_D -.->|不比较| NEW_F OLD_E -.->|不比较| NEW_F style OLD_D fill:#f66,stroke:#333 style OLD_E fill:#f66,stroke:#333
// 同层比较的实现逻辑
function reconcileChildren(oldFiber, newElements) {
let index = 0;
let oldFiber = oldFiber;
let prevSibling = null;
while (index < newElements.length || oldFiber != null) {
const newElement = newElements[index];
const newFiber = updateSlot(oldFiber, newElement);
// 同一位置比较,不跨层级
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
index++;
}
}

3.3 key 的作用与原理#

key 是 React 追踪列表元素身份的关键:

sequenceDiagram participant Old as 旧列表 [A, B, C] participant Diff as Diff 算法 participant New as 新列表 [A, C, B] Note over Old,New: 无 key 的情况 Old->>Diff: 索引 0: A Diff->>New: 索引 0: A(匹配) Old->>Diff: 索引 1: B Diff->>New: 索引 1: C(不匹配,更新) Note over Diff: 复用节点,更新内容 B → C Old->>Diff: 索引 2: C Diff->>New: 索引 2: B(不匹配,更新) Note over Diff: 复用节点,更新内容 C → B Note over Old,New: 有 key 的情况 Old->>Diff: key: a → A Diff->>New: key: a → A(匹配) Old->>Diff: key: b → B Diff->>New: key: c → C(不同 key) Note over Diff: 移动 B 到位置 2 Old->>Diff: key: c → C Diff->>New: key: b → B(移动 C 到位置 1) Note over Diff: 只移动,不更新内容
// 无 key:低效更新
<ul>
{items.map((item) => (
<li>{item.text}</li>
))}
</ul>
// 有 key:高效移动
<ul>
{items.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>

3.4 Diff 算法的完整流程#

flowchart TD A[开始 Diff] --> B{节点类型是否相同?} B -->|不同| C[销毁旧节点<br/>创建新节点] B -->|相同| D{是否有子节点?} D -->|都无| E[更新属性] D -->|旧有新无| F[删除旧子节点] D -->|旧无新有| G[创建新子节点] D -->|都有| H[递归 Diff 子节点] H --> I{子节点 Diff} I --> J[比较 key] J --> K{key 是否匹配?} K -->|匹配| L[复用节点<br/>更新属性] K -->|不匹配| M{是否可复用?} M -->|可复用| N[移动节点] M -->|不可复用| O[创建新节点] C --> P[返回更新结果] E --> P F --> P G --> P L --> P N --> P O --> P

3.5 时间复杂度分析#

场景朴素 DiffReact Diff说明
两个相同树O(n)O(n)遍历比较
完全不同的树O(n³)O(n)React 直接重建
列表顺序变化O(n²)O(n)key 优化
列表插入/删除O(n²)O(n)key 优化

四、批量更新与异步渲染#

4.1 批量更新的原理#

React 将多次 setState 合并为一次更新:

sequenceDiagram participant Code as 用户代码 participant React as React 更新队列 participant DOM as 真实 DOM Code->>React: setState({ count: 1 }) Note over React: 加入队列,不立即更新 Code->>React: setState({ count: 2 }) Note over React: 加入队列,不立即更新 Code->>React: setState({ count: 3 }) Note over React: 加入队列,不立即更新 Note over React: 事件处理结束<br/>批量处理队列 React->>React: 合并状态更新 React->>DOM: 一次渲染更新 Note over DOM: 只触发一次重排重绘
class Counter extends React.Component {
state = { count: 0 };
handleClick = () => {
// 这三次 setState 会合并
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
// 此时 state.count 仍然是 0!
console.log(this.state.count); // 0
// 在 React 18 的 automatic batching 中
// 即使在 setTimeout/Promise 中也会批量更新
};
}

4.2 React 18 的自动批量更新#

flowchart TB subgraph React 17 及之前 A1[事件处理函数内] --> B1[批量更新 ] A2[setTimeout/Promise] --> B2[不批量更新 ] A3[原生 DOM 事件] --> B3[不批量更新 ] end subgraph React 18 Automatic Batching C1[事件处理函数内] --> D1[批量更新 ] C2[setTimeout/Promise] --> D2[批量更新 ] C3[原生 DOM 事件] --> D3[批量更新 ] C4[React Native] --> D4[批量更新 ] end style B2 fill:#f66,stroke:#333 style B3 fill:#f66,stroke:#333 style D1 fill:#9f6,stroke:#333 style D2 fill:#9f6,stroke:#333 style D3 fill:#9f6,stroke:#333 style D4 fill:#9f6,stroke:#333

4.3 并发渲染(Concurrent Rendering)#

React 18 引入的并发渲染允许 React 中断渲染:

flowchart TB subgraph 同步渲染(React 17) S1[开始渲染] --> S2[渲染组件树] S2 --> S3[完成渲染] Note over S1,S3: 渲染期间阻塞主线程<br/>用户交互无响应 end subgraph 并发渲染(React 18) C1[开始渲染] --> C2[渲染部分] C2 --> C3{有更高优先级任务?} C3 -->|是| C4[暂停渲染<br/>处理用户交互] C4 --> C5[恢复渲染] C3 -->|否| C5 C5 --> C6[完成渲染] Note over C1,C6: 可中断、可恢复<br/>响应用户交互 end style S2 fill:#f66,stroke:#333 style C4 fill:#9f6,stroke:#333
// 使用 useTransition 标记低优先级更新
function SearchResults() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = e => {
// 高优先级:立即更新输入框
setQuery(e.target.value);
// 低优先级:可以延迟的搜索结果更新
startTransition(() => {
setResults(search(e.target.value));
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<Results data={results} />
</div>
);
}

4.4 更新优先级系统#

flowchart TB subgraph 优先级层次 P1[Immediate<br/>用户输入、点击] P2[User Blocking<br/>悬浮、拖拽] P3[Normal<br/>数据获取、渲染] P4[Low<br/>分析、预加载] P5[Idle<br/>日志、清理] end P1 --> P2 --> P3 --> P4 --> P5 style P1 fill:#f66,stroke:#333 style P2 fill:#f96,stroke:#333 style P3 fill:#ff9,stroke:#333 style P4 fill:#9f9,stroke:#333 style P5 fill:#9cf,stroke:#333

五、虚拟 DOM vs 直接 DOM 操作性能对比#

5.1 性能对比实验#

一个实际性能测试的结果:

xychart-beta title "创建 10000 个列表项的性能对比(毫秒)" x-axis ["innerHTML", "documentFragment", "虚拟DOM(首次)", "虚拟DOM(更新)", "虚拟DOM(无变化)"] y-axis "时间(ms)" 0 --> 150 bar [45, 52, 120, 35, 5]
操作类型innerHTMLDocumentFragment虚拟 DOM(首次)虚拟 DOM(更新)
首次渲染 10000 项45ms52ms120ms-
更新其中 100 项50ms55ms35ms35ms
更新其中 1 项48ms50ms2ms2ms
无变化重新渲染45ms50ms5ms5ms

5.2 为什么首次渲染较慢?#

flowchart LR subgraph innerHTML H1[字符串拼接] --> H2[解析 HTML] H2 --> H3[创建 DOM] H3 --> H4[挂载] end subgraph 虚拟 DOM V1[创建 VNode] --> V2[创建 Fiber] V2 --> V3[Diff 算法] V3 --> V4[创建 DOM] V4 --> V5[挂载] end style V3 fill:#f96,stroke:#333 style V2 fill:#f96,stroke:#333

首次渲染时,虚拟 DOM 需要额外的创建和遍历开销。但这些开销换来的是:

  1. 更新时的性能优势:精确计算最小变更集
  2. 开发效率:声明式编程模型
  3. 一致性保证:可预测的更新行为

5.3 虚拟 DOM 更适合的场景#

flowchart TB A{应用特征} --> B[频繁状态变化] A --> C[复杂 UI 结构] A --> D[需要跨平台] A --> E[团队协作开发] B --> F[虚拟 DOM 优势明显] C --> F D --> F E --> F A --> G[静态页面] A --> H[极致性能要求] A --> I[简单交互] G --> J[直接 DOM 更合适] H --> J I --> J style F fill:#9f6,stroke:#333 style J fill:#ff9,stroke:#333

5.4 虚拟 DOM 的性能边界#

// 性能敏感场景:虚拟 DOM 的开销可能成为瓶颈
function LargeList({ items }) {
// 每帧都创建大量虚拟 DOM 节点
return (
<div>
{items.map((item, i) => (
// 10000+ 个组件实例
<ComplexItem key={i} data={item} />
))}
</div>
);
}
// 优化方案:使用虚拟列表
import { FixedSizeList } from "react-window";
function OptimizedList({ items }) {
return (
<FixedSizeList height={600} itemCount={items.length} itemSize={50}>
{({ index, style }) => (
<div style={style}>
<ComplexItem data={items[index]} />
</div>
)}
</FixedSizeList>
);
}

六、虚拟 DOM vs Svelte 编译时优化#

6.1 两种优化思路#

flowchart TB subgraph React 运行时优化 R1[源代码] --> R2[编译为 JS] R2 --> R3[运行时虚拟 DOM] R3 --> R4[Diff 算法] R4 --> R5[DOM 更新] Note over R1,R5: 通用运行时<br/>框架处理所有情况 end subgraph Svelte 编译时优化 S1[源代码] --> S2[编译器分析] S2 --> S3[生成精确更新代码] S3 --> S4[直接 DOM 操作] Note over S1,S4: 编译时优化<br/>生成针对性代码 end style R3 fill:#f96,stroke:#333 style R4 fill:#f96,stroke:#333 style S3 fill:#9f6,stroke:#333 style S4 fill:#9f6,stroke:#333

6.2 Svelte 的编译策略#

Svelte 在编译时分析代码,生成直接的 DOM 更新指令:

<!-- Svelte 源代码 -->
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
Count: {count}
</button>

编译后生成的更新代码(简化版):

// Svelte 生成的更新函数
function update(changed, ctx) {
if (changed.count) {
text_node.data = "Count: " + ctx.count;
}
}

6.3 性能对比#

xychart-beta title "框架性能对比(Benchmark 得分,越高越好)" x-axis ["Vanilla JS", "Svelte", "Vue 3", "React 18"] y-axis "得分" 0 --> 1.5 bar [1.0, 1.2, 1.0, 0.9]
维度React(虚拟 DOM)Svelte(编译时)说明
首次渲染较慢无运行时开销
更新性能更好精确更新
运行时大小较大(~40KB)极小(按需)Svelte 无框架运行时
开发体验成熟生态快速迭代中React 生态更完善
调试能力DevTools 完善相对较弱React 工具链成熟
跨平台React NativeSvelte NativeReact 跨平台更成熟

6.4 设计权衡#

flowchart TB subgraph React 的选择 R1[运行时通用性] --> R2[接受一定运行时开销] R2 --> R3[换取开发灵活性] R3 --> R4[跨平台能力] end subgraph Svelte 的选择 S1[编译时特化] --> S2[最优运行时性能] S2 --> S3[最小运行时体积] S3 --> S4[牺牲部分灵活性] end style R3 fill:#9f6,stroke:#333 style R4 fill:#9f6,stroke:#333 style S2 fill:#9f6,stroke:#333 style S3 fill:#9f6,stroke:#333

React 团队的选择理由:

  1. 编译时优化的局限:无法处理动态场景(如根据数据渲染不同组件)
  2. 跨平台需求:React Native 需要统一的抽象层
  3. 生态一致性:运行时抽象保证了不同环境的兼容性
  4. 渐进增强:可以在运行时优化之上叠加编译时优化

七、Fiber 架构的演进#

7.1 React 15 的问题:栈调和器#

React 15 使用递归遍历虚拟 DOM 树:

flowchart TD subgraph React 15 栈调和器 A[开始更新] --> B[递归遍历整棵树] B --> C[同步执行所有工作] C --> D[渲染完成] Note over B: 无法中断<br/>主线程阻塞 end style B fill:#f66,stroke:#333 style C fill:#f66,stroke:#333
// React 15 的递归更新(简化)
function reconcile(parent, oldNode, newNode) {
// 递归遍历,无法中断
if (oldNode.type !== newNode.type) {
// 创建新节点...
}
// 递归处理子节点
for (let i = 0; i < newNode.children.length; i++) {
reconcile(parent, oldNode.children[i], newNode.children[i]);
}
// 整个过程是同步的
}

问题:一旦开始更新,必须完成整个树的遍历。大应用可能造成明显的卡顿。

7.2 Fiber 的核心设计#

React 16 重写了核心算法,引入 Fiber 架构:

flowchart TB subgraph Fiber 节点结构 F1[type: 元素类型] F2[key: 标识] F3[props: 属性] F4[stateNode: DOM/实例] F5[return: 父节点] F6[child: 第一个子节点] F7[sibling: 下一个兄弟] F8[effectTag: 副作用标记] end subgraph 工作单元 W1[当前 Fiber] W2[执行工作] W3[生成子 Fiber] W4[返回下一个工作单元] end F1 --> W1 W1 --> W2 --> W3 --> W4
// Fiber 节点结构(简化)
const fiber = {
type: "div", // 元素类型
key: null, // 标识
props: { className: "container" }, // 属性
// 树结构
return: parentFiber, // 父节点
child: firstChildFiber, // 第一个子节点
sibling: nextSiblingFiber, // 下一个兄弟
// 状态
alternate: oldFiber, // 上一次的 Fiber
effectTag: "UPDATE", // 副作用标记
// 优先级
lanes: priority, // 更新优先级
};

7.3 可中断渲染#

Fiber 架构实现了可中断的渲染:

sequenceDiagram participant Main as 主线程 participant Scheduler as 调度器 participant Fiber as Fiber 树 Main->>Scheduler: 触发更新 Scheduler->>Fiber: 开始渲染 loop 每个工作单元 Fiber->>Fiber: 处理一个节点 Fiber->>Scheduler: 检查时间片 alt 时间片用完 Scheduler->>Main: 让出控制权 Note over Main: 处理用户输入等 Main->>Scheduler: 请求继续 Scheduler->>Fiber: 继续渲染 else 时间片剩余 Fiber->>Fiber: 继续处理下一个节点 end end Fiber->>Main: 渲染完成

7.4 双缓冲技术#

React 使用两棵 Fiber 树实现双缓冲:

flowchart TB subgraph 当前树 Current Tree C1[FiberRoot] C2[App] C3[Header] C4[Content] end subgraph 工作树 WorkInProgress Tree W1[FiberRoot] W2[App'] W3[Header'] W4[Content'] end C1 -.->|alternate| W1 C2 -.->|alternate| W2 C3 -.->|alternate| W3 C4 -.->|alternate| W4 W4 -->|完成后交换指针| C4 Note over C1,C4: 显示中的树<br/>不可修改 Note over W1,W4: 正在构建的树<br/>可以修改 style C1 fill:#9cf,stroke:#333 style W1 fill:#ff9,stroke:#333

双缓冲的优势

  1. 原子性更新:要么全部成功,要么全部回滚
  2. 中断恢复:可以随时暂停和继续
  3. 一致性视图:用户始终看到完整的 UI

7.5 Fiber 架构的性能收益#

特性React 15React 16+ Fiber收益
渲染可中断响应更快
优先级调度重要更新优先
暂停/恢复更好的用户体验
错误边界局部完善更好的错误处理
大组件树卡顿明显可控渐进式渲染

八、跨平台能力(React Native)#

8.1 虚拟 DOM 作为跨平台抽象#

虚拟 DOM 的真正威力在于它提供了一个与平台无关的 UI 抽象层

flowchart TB subgraph React 应用代码 JSX[JSX 组件] end subgraph React 核心 VDOM[虚拟 DOM] Reconciler[调和器] end subgraph 渲染器 DOM[ReactDOM<br/>Web 渲染器] Native[React Native<br/>iOS/Android 渲染器] ART[React ART<br/>Canvas 渲染器] Test[React Test<br/>测试渲染器] Three[React Three Fiber<br/>3D 渲染器] end JSX --> VDOM --> Reconciler Reconciler --> DOM Reconciler --> Native Reconciler --> ART Reconciler --> Test Reconciler --> Three style VDOM fill:#9cf,stroke:#333 style Reconciler fill:#9cf,stroke:#333

8.2 React Native 的实现原理#

flowchart LR subgraph JavaScript 线程 JS[React 组件] VDOM[虚拟 DOM 树] B[Bridge] end subgraph Native 线程 Shadow[Shadow Tree] Native[Native 组件] end JS --> VDOM --> B B -->|序列化更新| Shadow Shadow -->|计算布局| Native Note over B: 异步消息队列<br/>JSON 序列化传输 style B fill:#f96,stroke:#333
// 同一套 React 代码
function App() {
return (
<View style={styles.container}>
<Text>Hello, World!</Text>
<Button title="Click me" onPress={handlePress} />
</View>
);
}
// Web 平台:渲染为 <div>, <span>, <button>
// iOS 平台:渲染为 UIView, UILabel, UIButton
// Android 平台:渲染为 ViewGroup, TextView, Button

8.3 跨平台的代价与收益#

方面收益代价
开发效率一套代码多平台运行需要学习平台特定知识
代码复用业务逻辑完全复用UI 层可能需要平台适配
性能接近原生Bridge 通信有开销
更新速度热更新(无需应用商店审核)需要兼容性处理
生态共享 React 生态原生库需要封装

8.4 为什么原生框架难以取代 React Native?#

flowchart TB subgraph 原生开发 N1[iOS: Swift/Objective-C] N2[Android: Kotlin/Java] N3[两套代码库] N4[两套团队] end subgraph React Native R1[单一代码库] R2[JavaScript/TypeScript] R3[共享 React 生态] R4[快速迭代] end N1 --> N3 N2 --> N3 N3 --> N4 R1 --> R2 --> R3 --> R4 style N4 fill:#f96,stroke:#333 style R4 fill:#9f6,stroke:#333

九、声明式编程范式的优势#

9.1 命令式 vs 声明式#

flowchart TB subgraph 命令式编程 I1[描述"怎么做"] --> I2[手动管理状态变化] I2 --> I3[手动更新视图] I3 --> I4[容易出错] I4 --> I5[难以维护] end subgraph 声明式编程 D1[描述"是什么"] --> D2[状态驱动视图] D2 --> D3[自动更新视图] D3 --> D4[可预测] D4 --> D5[易于维护] end style I4 fill:#f66,stroke:#333 style I5 fill:#f66,stroke:#333 style D4 fill:#9f6,stroke:#333 style D5 fill:#9f6,stroke:#333

9.2 代码复杂度对比#

实现一个简单的条件渲染:

// 命令式(jQuery)
function updateUI(user) {
if (user.isLoggedIn) {
$("#login-form").hide();
$("#user-profile").show();
$("#user-name").text(user.name);
if (user.isAdmin) {
$("#admin-panel").show();
} else {
$("#admin-panel").hide();
}
} else {
$("#login-form").show();
$("#user-profile").hide();
$("#admin-panel").hide();
}
}
// 声明式(React)
function UserProfile({ user }) {
if (!user.isLoggedIn) {
return <LoginForm />;
}
return (
<div>
<Profile name={user.name} />
{user.isAdmin && <AdminPanel />}
</div>
);
}

9.3 可预测性和可测试性#

flowchart LR subgraph React 组件 A[Props + State] --> B[渲染函数] B --> C[UI 输出] end subgraph 测试 T1[给定 Props] --> T2[调用渲染] T2 --> T3[断言输出] end A --> T1 C --> T3 style B fill:#9cf,stroke:#333
// React 组件测试:纯函数般简单
test("UserProfile shows admin panel for admin user", () => {
const user = { isLoggedIn: true, name: "Admin", isAdmin: true };
render(<UserProfile user={user} />);
expect(screen.getByText("Admin")).toBeInTheDocument();
expect(
screen.getByRole("region", { name: "Admin Panel" })
).toBeInTheDocument();
});

9.4 心智模型的简化#

方面命令式编程声明式编程
状态管理分散在多处集中在 state/props
UI 更新手动触发自动响应状态变化
代码阅读需要理解执行流程直接看渲染函数
Bug 排查难以定位状态不一致状态与 UI 对应关系清晰
重构容易引入 Bug修改渲染函数即可
团队协作需要理解全部代码组件边界清晰

十、总结与权衡#

10.1 虚拟 DOM 的核心价值#

mindmap root((虚拟 DOM 价值)) 开发效率 声明式编程 组件化架构 状态驱动视图 性能保障 批量更新 最小化 DOM 操作 避免常见陷阱 跨平台能力 React Native 服务端渲染 多端一致性 生态建设 丰富的组件库 开发工具链 社区支持

10.2 虚拟 DOM 的代价#

代价类型具体表现应对策略
内存开销维护虚拟 DOM 树现代设备内存充足
CPU 开销Diff 计算成本算法优化、并发渲染
首次渲染性能比直接 DOM 操作慢服务端渲染、预编译
学习曲线需要理解新范式文档完善、社区支持
调试复杂性状态追踪有时困难DevTools 支持

10.3 适用场景判断#

flowchart TD A{选择技术方案} --> B{项目规模} B -->|小型| C{交互复杂度} B -->|中型/大型| D[推荐 React] C -->|简单| E[原生 JS/jQuery 足够] C -->|复杂| F{性能要求} F -->|极致| G{考虑 Svelte/其他方案} F -->|标准| D A --> H{是否需要跨平台} H -->|是| I[React + React Native] H -->|否| D style D fill:#9f6,stroke:#333 style I fill:#9f6,stroke:#333

10.4 虚拟 DOM 不是银弹#

虚拟 DOM 是一种权衡,而不是银弹:

  1. 不是最快:编译时优化(如 Svelte)在特定场景更快
  2. 不是最小:运行时增加了包体积
  3. 不是万能:对极致性能场景需要额外优化
  4. 但它是务实的:在大多数场景提供了良好的开发体验和可接受的性能

10.5 React 团队的核心洞察#

React 团队在设计虚拟 DOM 时的核心洞察:

开发者时间比 CPU 时间更宝贵。

虚拟 DOM 的价值不在于它比原生 DOM 操作更快,而在于:

  1. 提供一致的编程模型:开发者不需要关心 DOM 操作的细节
  2. 避免常见错误:批量更新、避免强制同步布局等
  3. 实现跨平台:一套代码,多端运行
  4. 支撑生态发展:稳定的 API 促进工具链和组件库发展

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为什么 React 使用虚拟 DOM
https://blog.souloss.com/posts/why-the-design/why-react-uses-virtual-dom/
作者
Souloss
发布于
2024-02-09
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时