在上一篇中,我们了解了 Vue2 的响应式原理,它实现了数据变化自动驱动视图更新。那么,当数据变化后,Vue2 是如何高效地更新真实 DOM 的呢?本篇将带你深入理解 Vue2 的 虚拟 DOM 与 Diff 算法。
目录
- [什么是虚拟 DOM](#什么是虚拟 DOM)
- [为什么需要虚拟 DOM](#为什么需要虚拟 DOM)
- [Vue2 中的虚拟 DOM](#Vue2 中的虚拟 DOM)
- [Diff 算法](#Diff 算法)
- 小结
什么是虚拟 DOM
虚拟 DOM (Virtual DOM) 是用 JavaScript 对象来表示真实 DOM 的一种方式。 它本质上是真实 DOM树的一个"轻量级"副本,通过 JS 对象描述 UI 结构,而非直接操作浏览器 DOM。 虚拟 DOM 的作用是:当数据变化时,先在内存中比较虚拟 DOM 的差异(Diff),再将最小化的变更应用到真实 DOM 上,从而避免频繁的直接 DOM操作。
例子:一个简单的虚拟 DOM 表示一个 div 元素,包含一个 p 子元素:
html
<div id="app">
<p>Hello World</p>
</div>
对应的虚拟 DOM 对象:
js
{
tag: 'div',
data: { id: 'app' },
children: [
{
tag: 'p',
children: [{ text: 'Hello World' }]
}
]
}
通过这种对象表示,Vue 可以快速创建、比较和更新 UI,而无需立即触及昂贵的真实 DOM 操作。
为什么需要虚拟 DOM
直接操作真实 DOM 成本很高,因为浏览器需要重新计算布局、样式和重绘页面。每次小变化都可能导致全量重绘,性能低下。
虚拟 DOM 的引入解决了这些问题:
- 批量、最小化更新:数据变化时,先在 JS 层面计算差异,只更新必要的真实 DOM 部分。
- 提升跨平台能力:虚拟 DOM 不依赖浏览器,可以用于 Native 应用、服务器端渲染 (SSR) 等场景。
- 抽象层:开发者无需关心底层 DOM 差异,框架统一处理。
在 Vue2 中,虚拟 DOM 是响应式系统与视图更新的桥梁:响应式检测到数据变化后,生成新的虚拟 DOM 树,然后通过 Diff 算法高效 patch 到真实 DOM。
Vue2 中的虚拟 DOM
在 Vue2 中,虚拟 DOM 节点 (VNode) 是一个普通的 JS 对象,包含标签、属性、子节点等信息。
Vue 会将模板编译成渲染函数,这个函数返回虚拟 DOM 树。每个 VNode 对象结构大致如下:
js
const vnode = {
tag: 'div', // 标签名
data: { // 属性和数据
id: 'app',
class: 'container'
},
children: [ // 子节点数组
{
tag: 'p',
text: 'Hello Vue!', // 文本节点
children: []
}
],
text: undefined // 如果是纯文本节点
}
当组件初始化或数据更新时,Vue 调用渲染函数生成新的 VNode 树,与旧树进行比较。
这种对象表示轻量高效,便于 JS 引擎优化。
Diff 算法
Diff 算法是虚拟 DOM 的核心,用于比较新旧
VNode
树,找出最小变更集。Vue2 的 Diff 算法采用 同层比较策略,只在同一层级进行比较,不会跨层级移动节点,从而简化计算并提升性能。
核心原则:
- 同层比较:只比较当前层级的节点,不会跨层级对比(例如,不会将 div 的子元素与 span 的子元素比较)。
- 通过 key 标识优化:使用 key 属性标识节点,实现精准复用,避免不必要的创建/删除。
伪代码示例(简化版):
js
function diff(oldVNode, newVNode) {
if (oldVNode.tag !== newVNode.tag) {
// 标签不同,直接替换整个节点
replaceNode(oldVNode, newVNode)
} else {
// 标签相同,比较属性和子节点
patchProps(oldVNode.data, newVNode.data)
diffChildren(oldVNode.children, newVNode.children)
}
}
function diffChildren(oldChildren, newChildren) {
// 遍历新旧子节点列表
for (let i = 0; i < newChildren.length; i++) {
let oldChild = findMatchingChild(oldChildren, newChildren[i], i)
if (oldChild && oldChild.key === newChildren[i].key) {
// key 相同,复用节点并递归 Diff
diff(oldChild, newChildren[i])
} else {
// 不同,创建新节点或删除旧节点
createNode(newChildren[i])
}
}
}
在实际实现中,Vue2 使用双端指针(head-tail 比较)优化列表 Diff,时间复杂度为 O(n),远优于全量重建的 O(n³)。
注意:key 的正确使用至关重要,它帮助 Diff 算法准确识别节点,避免列表重排序时的性能问题。
小结
通过引入虚拟 DOM 和 Diff 算法,Vue2 成功地优化了视图更新过程,减少了不必要的真实 DOM 操作,从而提高了性能。虚拟 DOM 提供了一个高效、跨平台的方式来处理 UI 更新,而 Diff 算法则确保了最小化的变更集被应用到真实 DOM 中。
📗 下一篇进阶文章,我们将学习 模板编译原理,理解 Vue 如何将模板编译成虚拟 DOM。