vault backup: 2026-05-27 21:58:46
This commit is contained in:
@@ -0,0 +1,191 @@
|
|||||||
|
---
|
||||||
|
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 性能优化策略]]
|
||||||
Reference in New Issue
Block a user