Files
cs-note/hzh/REACT/虚拟 DOM 和 真实 DOM 有什么区别.md
T

192 lines
6.1 KiB
Markdown
Raw Normal View History

2026-05-27 21:58:46 +08:00
---
tags: [React, 前端, 虚拟DOM, DOM]
create time: 2026-05-26 15:30
---
# 虚拟 DOM 和 真实 DOM 有什么区别
## 概述
虚拟 DOM(Virtual DOM)是 React 的"秘密武器"——它先用 JavaScript 对象在内存中画一张 **UI 蓝图**,然后通过一套高效的 Diff 算法找出改动最少的那部分,最后一次性刷到真实的页面上。这样既省去了频繁操作浏览器 DOM 的性能损耗,又保留了声明式编写 UI 的方便。
## 正文
### 先搞懂:什么是"真实 DOM"?
浏览器加载网页后,会把 HTML 文档转换成一棵节点树,这就是 **DOM 树**。每个 `<div>`、每段文字都是树上的一个节点。
```html
<ul id="list">
<li>苹果</li>
<li>香蕉</li>
<li>橙子</li>
</ul>
```
对应的 DOM 树结构:
```mermaid
graph TD
UL["<ul id='list'>"] --> LI1["<li>苹果</li>"]
UL --> LI2["<li>香蕉</li>"]
UL --> LI3["<li>橙子</li>"]
```
**问题来了**:如果我想在"香蕉"前面加一个"草莓",传统写法是:
```tsx
// ❌ 传统写法:手动改 DOM
const list = document.getElementById("list");
const strawberry = document.createElement("li");
strawberry.textContent = "草莓";
list.insertBefore(strawberry, list.children[1]);
```
看起来简单?但当页面有 100+ 个元素要同时更新时,这种逐条操作的方式会频繁触发浏览器的 **重排(Reflow)** 和 **重绘(Repaint)**,体验明显卡顿。
> [!QUESTION] 为什么多次修改 DOM 这么慢?
> 想象你在白板上画图:每擦一次就重新描一遍。而批量操作的思路是先画草稿纸,确认没问题了再一次性誊到白板上。
### 虚拟 DOM:DOM 树的"替身演员"
虚拟 DOM 不是新东西——它只是 **用 JS 对象模拟 DOM 树的一份副本**:
```tsx
// JSX 写法(你写的)
const element = <ul id="list"><li>苹果</li><li>香蕉</li></ul>;
// React 内部实际生成的虚拟 DOM(纯 JS 对象)
const vdom = {
type: "ul",
props: { id: "list", children: [
{ type: "li", props: { children: ["苹果"] } },
{ type: "li", props: { children: ["香蕉"] } }
]}
};
```
这个对象 **不占用任何浏览器资源**,只在 JS 内存里跑。你可以把它理解为装修的 3D 效果图——你看的是图,但房子还没动。
| | 真实 DOM | 虚拟 DOM |
|---|---------|---------|
| 是什么 | 浏览器的 DOM 节点对象 | JS 普通对象 |
| 在哪运行 | 浏览器引擎(C++) | JS 引擎(V8) |
| 改动成本 | 高(触发布局计算) | 低(只是对象赋值) |
| 能跨平台吗 | ❌ 只能浏览器 | ✅ SSR、React Native |
### 核心原理:Diff + Patch
虚拟 DOM 的威力不在"虚拟"本身,而在它的 **对比机制**。流程只有三步:
```mermaid
flowchart LR
A["状态变化"] --> B["生成新的虚拟 DOM"]
B --> C["新旧对比(Diff)"]
C --> D["只更新变动的部分(Patch)"]
style A fill:#e1f5fe
style B fill:#fff3e0
style C fill:#fce4ec
style D fill:#e8f5e9
```
#### 举个例子:添加一项水果
```tsx
// 第 1 步:原始列表 —— 用户点了"添加草莓"
setState({ fruits: ["苹果", "香蕉", "橙子"] })
// ↓
// 🍎 🍌 🍊
// 第 2 步:React 生成新的虚拟 DOM
setState({ fruits: ["苹果", "香蕉", "草莓", "橙子"] })
// ↓
// 🍎 🍌 🍓 🍊
// 第 3 步:Diff 发现只有 1 处变化
// Patch 只需执行 1 次操作:<li>草莓</li> 插入到香蕉后面
```
对比两种方式的差异:
```
直接操作 DOM:
├── insertBefore() ← 触发重排
├── 可能需要调整后续索引 ← 额外逻辑
React 的 VDOM:
├── 创建新 JS 对象 ← 纯内存操作,极快
├── Diff 比较两棵树 ← 内存中运算,也不影响渲染
└── 批量应用变更 ← 浏览器只收到一条指令
```
> [!TIP] CPU 换 GPU 思维
> 有人问:"Diff 本身也要算啊,不是多了一步吗?"——对,VDOM 确实多了一次 CPU 计算,但它把 N 次昂贵的 GPU(浏览器渲染管线的重排/重绘)合并成 1 次。**用便宜的算力换取昂贵的渲染,这是现代前端的经典权衡。**
#### Diff 是怎么做到这么快?
暴力对比两棵树的复杂度是 O(n³),不可接受。React 做了两个"偷懒"的设计把它们砍到 O(n):
1. **同层对比**:不同层的节点绝不拿来比,类型一变就直接销毁重建
2. **key 属性**:列表项靠 key 来辨认身份,避免误判
```
假设列表从 [A, B, C] 变成 [B, A, C]:
暴力对比:逐个位置看,认为 3 个都变了 ❌
React Diff:通过 key 识别出 B 和 A 只是换了位置,只需交换 ✅
```
### key 到底是什么?为什么非得加?
`key` 就是虚拟 DOM 对比时的 **身份证**。
```tsx
// ✅ 正确:用不变的 ID 当 key
users.map(user => <li key={user.id}>{user.name}</li>)
// ❌ 错误:用 index 当 key
users.map((user, i) => <li key={i}>{user.name}</li>)
```
**为什么不能用 index?** 看这个场景:
```
初始列表:Alice(0), Bob(1), Carol(2)
删除 Alice 后:Bob 变成了 [0],Carol 变成了 [1]
用 index 当 key:
Diff 看到 index=0 的值从 "Alice" 变成了 "Bob"
→ 认为整个列表全变了,全部重建 ❌
用 user.id 当 key:
Diff 看到 id=1 的 "Bob" 只是位置上移了一位
→ 只做移动,不重建 ✅
```
> [!NOTE] 什么时候可以用 index?
> 列表完全不会增删排序、且项之间没有任何状态交互——几乎不存在这样的场景。安全起见,永远用稳定唯一 ID。
### 完整工作流程
```mermaid
sequenceDiagram
participant U as 用户
participant R as React
participant V as 旧虚拟 DOM
participant New as 新虚拟 DOM
participant P as Diff 引擎
participant D as 真实 DOM
U->>R: 点击按钮 / setState
R->>New: 用最新数据生成新虚拟 DOM
R->>P: 传入旧虚拟 DOM 和新虚拟 DOM
P->>P: 逐层对比,记录最小变更集
P-->>R: 返回需要更新的位置
R->>D: 批量写入变更
D-->>U: 页面刷新
```
## 关联笔记
- [[REACT/React 的生命周期和 Hooks]]
- [[REACT/React 性能优化策略]]