一、虚拟 DOM(Virtual DOM)
1. 是什么
虚拟 DOM 本质就是一个 JS 对象,用来描述真实 DOM 结构。
例如 JSX:
xml
<div className="box">
<span>Hello</span>
</div>
会被转换成类似:
css
{
type: 'div',
props: {
className: 'box',
children: [
{
type: 'span',
props: {
children: 'Hello'
}
}
]
}
}
2. 为什么要有虚拟 DOM?
核心目的:减少真实 DOM 操作
因为:
- 真实 DOM 操作成本高(重排 / 重绘)
- JS 计算相对便宜
👉 所以 React 做了一层"中间层":
状态变化 → 生成新的虚拟DOM → Diff → 最小化更新真实DOM
二、Diff 算法原理
React 的 Diff 不是传统树算法(O(n³)),而是做了优化 → O(n)
核心基于 3 个假设:
1️⃣ 同层比较(不跨层)
👉 React 只比较同一层节点,不会跨层移动
例如:
css
A
├─ B
└─ C
如果变成:
css
A
└─ B
└─ C
React 会认为:
- C 被删除
- 新建一个 C
❗不会复用
👉 这是用空间换时间
2️⃣ 类型不同直接替换
css
<div />
→
<span />
👉 直接销毁旧节点,创建新节点
3️⃣ 列表使用 key 优化
👉 这是重点(和你下面的问题强相关)
三、key 的作用
本质作用:
👉 标识节点的唯一身份
让 React 在 Diff 时可以:
✔ 复用节点
✔ 只更新变化的部分
✔ 避免错误复用
举例
旧列表:
bash
[{id:1}, {id:2}, {id:3}]
新列表:
bash
[{id:3}, {id:1}, {id:2}]
❌ 没有 key(或用 index)
React 会按位置比较:
makefile
旧: 1 2 3
新: 3 1 2
👉 结果:
- 全部节点都被认为变了
- 全部重新渲染
✅ 使用 key
makefile
key: 1 2 3
→
key: 3 1 2
React 会:
👉 发现只是"顺序变了"
👉 复用节点,只移动 DOM
四、为什么不能用 index 作为 key?
很多人背这个结论,但不理解原因。
核心问题:index 不是稳定标识
场景 1:插入元素
旧:
csharp
[A, B, C]
key: 0 1 2
新(头部插入 D):
csharp
[D, A, B, C]
key: 0 1 2 3
React 看到的是:
css
旧 0 → 新 0 (A → D ❌)
旧 1 → 新 1 (B → A ❌)
旧 2 → 新 2 (C → B ❌)
👉 全错位
后果:
- 组件状态错乱(最严重问题)
- 输入框内容串位
- 动画异常
场景 2:删除元素
csharp
[A, B, C]
→
[A, C]
index 变化:
css
B 被删 → C 的 index 从 2 → 1
👉 React 误以为:
- B → C(复用错误)
场景 3:表单输入(经典面试题)
ini
<input value="A" />
<input value="B" />
删除第一个后:
👉 B 会变成 A(错位)
五、什么时候可以用 index?
不是绝对不能用,而是有条件:
👉 满足以下条件可以用:
- 列表不会发生顺序变化
- 没有插入 / 删除
- 只是静态展示
例如:
javascript
[1,2,3].map((item, index) => <li key={index}>{item}</li>)
✔ 安全
六、总结(面试版)
你可以这样说:
React 通过虚拟 DOM 来减少真实 DOM 操作,在状态更新时生成新的虚拟 DOM,然后通过 Diff 算法进行对比。
Diff 采用同层比较策略,并通过 key 来标识节点,提高复用效率。
key 的作用是帮助 React 识别节点是否可复用,如果使用 index 作为 key,在列表发生插入、删除、排序时会导致节点错位,可能引发状态错乱,因此不推荐使用。
七、给你一个更进阶的理解(加分项)
👉 React Diff 本质:
不是找"最优解",而是找"足够快的近似解"
👉 核心 trade-off:
精确性 ↓
性能 ↑