文章目录
- 前言
- [一、什么是虚拟 DOM](#一、什么是虚拟 DOM)
-
- [1.1 定义](#1.1 定义)
- [1.2 为什么需要虚拟 DOM](#1.2 为什么需要虚拟 DOM)
- [1.3 虚拟 DOM 的优势](#1.3 虚拟 DOM 的优势)
- [二、VNode 的结构](#二、VNode 的结构)
-
- [2.1 基本结构](#2.1 基本结构)
- [2.2 VNode 的类型](#2.2 VNode 的类型)
- 三、更新流程
-
- [3.1 完整流程](#3.1 完整流程)
- [3.2 简化的实现](#3.2 简化的实现)
- [3.3 简化的 Diff 算法](#3.3 简化的 Diff 算法)
- [四、Key 的作用与原理](#四、Key 的作用与原理)
-
- [4.1 定义](#4.1 定义)
- [4.2 应用场景](#4.2 应用场景)
- [4.3 Key 用 index 的问题](#4.3 Key 用 index 的问题)
- [4.4 易混淆点](#4.4 易混淆点)
- [五、Shadow DOM vs Virtual DOM](#五、Shadow DOM vs Virtual DOM)
-
- [5.1 区别](#5.1 区别)
- [5.2 Shadow DOM 示例](#5.2 Shadow DOM 示例)
- [六、虚拟 DOM 的性能](#六、虚拟 DOM 的性能)
-
- [6.1 虚拟 DOM 并非总是更快](#6.1 虚拟 DOM 并非总是更快)
- [6.2 Vue 3 的优化](#6.2 Vue 3 的优化)
- 七、易混淆点
- 八、思考与练习
- 总结
前言
上一篇讲了 Vue 响应式原理;本篇进入 虚拟 DOM------这是 React 和 Vue 等现代框架的核心机制。
虚拟 DOM 解决的核心问题是:如何高效地更新真实 DOM?
直接操作 DOM 很慢,但更慢的是不必要的 DOM 操作。虚拟 DOM 通过在内存中维护一棵 JavaScript 对象树,比较新旧两棵树的差异,然后只更新真正变化的部分,从而实现高效的 DOM 更新。
本篇会讲清楚:
- 虚拟 DOM 是什么?
- VNode 的结构是什么样的?
- 更新流程是怎样的?
key属性为什么重要?
一、什么是虚拟 DOM
1.1 定义
虚拟 DOM 是用 JavaScript 对象描述真实 DOM 结构的轻量级表示,每次状态变化生成新的虚拟 DOM 树。
javascript
// 真实 DOM
<div class="box">
<h1>Hello</h1>
<p>World</p>
</div>
// 虚拟 DOM(VNode)
const vnode = {
type: 'div',
props: { class: 'box' },
children: [
{ type: 'h1', children: 'Hello' },
{ type: 'p', children: 'World' }
]
}
1.2 为什么需要虚拟 DOM
问题:直接操作 DOM 很慢,而且很难追踪状态变化。
解决方案:
- 用 JavaScript 对象描述 DOM 结构(VNode)
- 状态变化时生成新的 VNode 树
- 比较新旧两棵树的差异(Diff)
- 只更新真正变化的部分到真实 DOM
1.3 虚拟 DOM 的优势
- 声明式 UI:开发者只需描述目标状态,框架自动处理 DOM 更新
- 批量更新:将多次状态变更合并为一次 DOM 更新
- 跨平台:同一套 VNode 树可渲染到浏览器 DOM、SSR HTML、Canvas 或移动端原生视图
- 可预测性:状态 → 视图的映射是确定性的,便于调试和测试
二、VNode 的结构
2.1 基本结构
javascript
// 简化的 VNode 结构
const vnode = {
type: 'div', // 标签名或组件
props: { // 属性
class: 'box',
onClick: handler
},
children: [ // 子节点
{ type: 'h1', children: 'Hello' },
{ type: 'p', children: 'World' }
],
key: 'unique-id', // 唯一标识(可选)
el: null // 真实 DOM 元素引用(运行时填充)
}
2.2 VNode 的类型
javascript
// 文本节点
{ type: null, children: 'Hello World' }
// 元素节点
{ type: 'div', props: {}, children: [] }
// 组件节点
{ type: MyComponent, props: { msg: 'hello' } }
// Fragment(Vue 3 支持多根节点)
{ type: Fragment, children: [...] }
三、更新流程
3.1 完整流程
1. 状态变化(响应式触发)
2. 生成新的 VNode 树
3. 与旧 VNode 树进行 Diff 比较
4. 计算出最小变更集
5. 批量应用到真实 DOM
3.2 简化的实现
javascript
// 创建 VNode
const h = (type, props, ...children) => ({
type,
props: props ?? {},
children: children.flat()
})
// 渲染 VNode 到真实 DOM
const render = (vnode, container) => {
const el = document.createElement(vnode.type)
// 设置属性
Object.entries(vnode.props).forEach(([key, value]) => {
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), value)
} else {
el.setAttribute(key, value)
}
})
// 递归渲染子节点
vnode.children.forEach(child => {
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child))
} else {
render(child, el)
}
})
container.appendChild(el)
}
// 使用
const app = h('div', { class: 'app' },
h('h1', null, 'Hello'),
h('p', null, 'World')
)
render(app, document.getElementById('root'))
3.3 简化的 Diff 算法
javascript
// 比较新旧 VNode
const diff = (oldVNode, newVNode) => {
// 节点类型不同,直接替换
if (oldVNode.type !== newVNode.type) {
return { type: 'REPLACE', newVNode }
}
// 文本节点内容变化
if (typeof newVNode.children === 'string' &&
oldVNode.children !== newVNode.children) {
return { type: 'TEXT', text: newVNode.children }
}
// 属性变化
const propPatches = diffProps(oldVNode.props, newVNode.props)
// 子节点变化
const childPatches = diffChildren(oldVNode.children, newVNode.children)
return { type: 'UPDATE', propPatches, childPatches }
}
四、Key 的作用与原理
4.1 定义
key是 Vue 在v-for列表渲染中用于标识每个节点的唯一属性,帮助 Diff 算法高效匹配新旧节点。- Vue 的 Diff 算法通过
key判断节点是否可以复用:相同 key 的节点进行 patch 比较,不同 key 则销毁重建。 - 不推荐使用
index作为 key,因为列表增删会导致索引重排,引起不必要的 DOM 重建和状态丢失。 key不仅用于v-for,在切换动态组件或触发过渡动画时也需要 key 来区分不同元素。
4.2 应用场景
- 列表渲染中使用唯一 ID(如
item.id)作为 key,保证列表增删时最小化 DOM 操作。 - 强制重新渲染组件:给同一组件切换不同的 key 值,Vue 会销毁旧实例并创建新实例。
- 表格行数据更新时,使用后端返回的业务 ID 作为 key,避免用户输入框内容错位。
4.3 Key 用 index 的问题
javascript
// ❌ 错误:使用 index 作为 key
<li v-for="(item, index) in list" :key="index">
<input v-model="item.name" />
</li>
// 问题:在列表头部插入新项时
// 旧: [A, B, C] → 新: [D, A, B, C]
// key=0: A → D (复用 A 的 DOM,但内容变成 D)
// key=1: B → A (复用 B 的 DOM,但内容变成 A)
// key=2: C → B (复用 C 的 DOM,但内容变成 B)
// key=3: 新增 C
// 结果:所有输入框的内容错位!
javascript
// ✅ 正确:使用唯一 ID 作为 key
<li v-for="item in list" :key="item.id">
<input v-model="item.name" />
</li>
// 结果:只有新增的 D 需要创建 DOM,其他节点正确复用
4.4 易混淆点
- 使用
index作 key 在列表只有追加操作时表现正常,但有插入/删除/排序时会出现性能问题和状态错乱。 - key 必须是唯一且稳定的值,使用
Math.random()作为 key 会导致每次渲染都重建所有节点。 key的作用不仅是优化性能,更重要的是保证组件状态的正确性和 DOM 复用的准确性。- Vue 3 的
v-for中key是必须的(会发出警告),Vue 2 中则是可选的。
五、Shadow DOM vs Virtual DOM
5.1 区别
| 对比项 | Shadow DOM | Virtual DOM |
|---|---|---|
| 定义 | 浏览器原生组件封装技术(Web Components) | 框架层的抽象 |
| 目的 | 封装组件内部结构和样式 | 高效更新 DOM |
| 实现 | 浏览器原生支持 | JavaScript 对象树 |
| 使用 | Web Components 标准 | React / Vue 等框架 |
5.2 Shadow DOM 示例
javascript
// Web Components 使用 Shadow DOM
class MyCard extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
.card { border: 1px solid #ccc; padding: 16px; }
</style>
<div class="card">
<slot></slot>
</div>
`
}
}
customElements.define('my-card', MyCard)
// 使用
// <my-card><p>Hello</p></my-card>
关键区别:Shadow DOM 是浏览器原生的组件封装技术,Virtual DOM 是框架层的抽象,两者完全无关。
六、虚拟 DOM 的性能
6.1 虚拟 DOM 并非总是更快
虚拟 DOM 的核心优势不在于"比直接操作 DOM 快",而在于:
- 声明式开发:开发者只需描述目标状态,框架自动计算最优更新
- 批量更新:将多次状态变更合并为一次 DOM 更新
- 可预测性:状态 → 视图的映射是确定性的
在极少量的 DOM 更新场景下,手动 DOM 操作可能更快。虚拟 DOM 的价值在于复杂场景下的批量更新和跨平台抽象。
6.2 Vue 3 的优化
Vue 3 的快速 Diff 算法借鉴了 Inferno,通过预处理最长递增子序列(LIS)优化移动操作:
javascript
// Vue 3 的编译时优化
// 1. 静态提升:静态节点只创建一次
// 2. Patch Flag:标记动态内容类型
// 3. Block Tree:只比较动态节点
七、易混淆点
- 虚拟 DOM ≠ 更快:对于简单场景(如修改一个文本),直接操作 DOM 可能更快;VNode 的优势在于复杂场景下的批量更新和跨平台抽象。
- Shadow DOM ≠ Virtual DOM:Shadow DOM 是浏览器原生组件封装技术(Web Components);Virtual DOM 是框架层的抽象,两者无关。
- Key 的作用:Diff 算法通过 key 判断节点是否可复用,key 不稳定(如用 index)会导致错误的节点复用和不必要的 DOM 操作。
- Vue 3 的优化:通过编译时优化(静态提升、Patch Flag、Block Tree)减少需要 Diff 的节点数。
八、思考与练习
1. 为什么 Vue 3 强制要求 v-for 必须有 key?
解析:key 帮助 Diff 算法高效识别节点的移动、新增和删除。没有 key 时,Vue 只能按顺序比较,可能导致错误的节点复用和状态丢失。
2. 使用 index 作为 key 在什么场景下会出问题?
解析:列表有插入、删除、排序操作时会出问题。例如在头部插入新项,所有节点的 index 都会变化,导致 DOM 复用错误。
3. 虚拟 DOM 的核心价值是什么?
解析:声明式开发 + 批量更新 + 跨平台。开发者只需描述目标状态,框架自动计算最优更新策略。
4. Vue 3 的编译时优化有哪些?
解析:
- 静态提升:静态节点只创建一次,后续复用
- Patch Flag:标记动态内容类型(文本、属性等),跳过静态内容
- Block Tree:只比较动态节点,减少 Diff 范围
5. Shadow DOM 和 Virtual DOM 有什么关系?
解析:没有关系。Shadow DOM 是浏览器原生的组件封装技术(Web Components),Virtual DOM 是框架层的抽象,两者目的和实现完全不同。
6. 如何强制重新渲染一个组件?
解析:给组件绑定不同的 key 值,Vue 会销毁旧实例并创建新实例:
vue
<Component :key="uniqueId" />
总结
- 虚拟 DOM 是用 JavaScript 对象描述真实 DOM 结构的轻量级抽象
- VNode 是虚拟 DOM 的基本单位,包含类型、属性、子节点等信息
- 更新流程:状态变化 → 生成新 VNode → Diff 比较 → 最小变更 → 更新 DOM
- Key 的作用:帮助 Diff 算法高效识别节点,保证状态正确性
- 虚拟 DOM 的价值:声明式开发、批量更新、跨平台,而非"比直接操作 DOM 快"