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

192 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 性能优化策略]]