深色模式
面试常被问到"Vue 的 diff 算法是怎么工作的",网上的文章大多说得很抽象。这里用实际例子说清楚。
为什么要有虚拟 DOM
操作真实 DOM 很慢,因为每次操作都可能触发浏览器的重排(layout)和重绘(paint)。
javascript
// 如果需要更新一个列表,最暴力的方式:
container.innerHTML = items.map((item) => `<li>${item.name}</li>`).join("");
// 问题:销毁了所有 DOM 节点,再创建新的,损失了 DOM 状态(如焦点、滚动位置)
// 理想方式:只更新变化的部分
// 虚拟 DOM 就是用 JS 对象模拟 DOM,通过对比新旧虚拟 DOM 找出最小差异虚拟 DOM 的结构
javascript
// 真实 DOM
// <div class="container">
// <ul>
// <li>Item 1</li>
// </ul>
// </div>
// 对应的虚拟 DOM(简化)
const vnode = {
tag: "div",
data: { class: "container" },
children: [
{
tag: "ul",
children: [{ tag: "li", children: [{ text: "Item 1" }] }],
},
],
};diff 算法的核心思路
Vue 的 diff 算法做了几个关键假设(来自 React 的 diff):
1. 只比较同层节点(不跨层比较)
→ 如果节点从 div A 移到 div B,视为删除后新增
→ 现实中 DOM 跨层移动非常少见,这个假设通常正确
2. 不同类型的节点,直接替换(不深入比较)
→ <div> 变成 <p>,直接替换整棵子树
3. 相同类型的节点,通过 key 判断是否是同一个节点
→ 没有 key:按位置比较
→ 有 key:匹配相同 key 的节点,实现重排列表 diff:双端比较
Vue 2 的列表 diff 用双端比较:同时从新旧列表的两端开始比较:
旧: [A, B, C, D]
新: [D, A, B, C]
步骤:
1. 新头(D) vs 旧头(A) → 不同
2. 新尾(C) vs 旧尾(D) → 不同
3. 新头(D) vs 旧尾(D) → 相同!把 D 移到前面
旧: [A, B, C](D 已处理)
新: [A, B, C]
4. 剩余:A=A, B=B, C=C → 都相同,不需要移动
结果:只需要移动 D,不需要重建整个列表key 的重要性
html
{% raw %}
<!-- ❌ 没有 key:按位置 diff,可能造成不必要的更新 -->
<li v-for="item in list">{{ item.name }}</li>
<!-- 假设 list 从 [A, B, C] 变成 [B, C](删除了 A)
Vue 会更新 li[0] 的内容(A→B),更新 li[1] 的内容(B→C),删除 li[2]
做了 2 次更新 + 1 次删除 -->
<!-- ✅ 有 key:精确匹配,只删除对应节点 -->
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
<!-- Vue 发现 B 和 C 没变,只删除 A
做了 1 次删除 -->
{% endraw %}key 的另一个作用:强制重新创建组件(用于清除状态):
html
<!-- 切换用户时,强制重新创建 UserProfile 组件,清除旧用户的状态 -->
<UserProfile :key="userId" :userId="userId" />小结
- 虚拟 DOM 是 JS 对象模拟的 DOM,通过 diff 找最小变化,减少真实 DOM 操作
- diff 的三个假设:同层比较、不同类型直接替换、用 key 匹配
- Vue 2 的列表 diff 用双端比较,效率较高
- 列表渲染务必加
:key,用稳定的唯一 id,不要用 index(除非列表不会变化)