在浏览器中,每一个页面元素都是真实 DOM 树上的一个节点。真实 DOM 节点非常“重”——它不仅包含了元素的标签名、属性、文本内容,还承载了大量浏览器引擎内部的渲染属性、事件系统、样式计算规则等。频繁地创建、修改或删除真实 DOM 节点,代价高昂,很容易成为性能瓶颈。
虚拟 DOM(Virtual DOM)的解决思路非常直接:用一个轻量级的 JavaScript 对象来“模拟”真实 DOM 节点,所有对界面的操作先在这个 JS 对象上进行,最后再一次性、最小化地更新到真实 DOM 中。
一个 VNode 长什么样
在 Vue 里,一个虚拟 DOM 节点通常被称为 VNode(Virtual Node)。Vue 3 的 VNode 大致结构如下:
const vnode = {
type: 'div', // 标签名,如 'div'、'p',也可以是组件对象
props: { // 属性、事件、样式等
id: 'app',
class: 'container',
onClick: handleClick
},
children: [ // 子节点数组,每个子节点也是 VNode
{
type: 'span',
props: null,
children: 'Hello'
}
],
el: null, // 对应的真实 DOM 节点,mount 之后由渲染器填充
key: null, // 用于列表 Diff 的唯一标识
patchFlag: 0, // 动态节点标记(编译优化生成)
dynamicProps: null, // 动态属性列表
// ... 其他内部字段
}
对比真实 DOM 中一个 <div> 节点的庞大属性集合,VNode 只保存了渲染所需的核心信息:标签名、属性、子节点、真实 DOM 引用。它是一个纯粹的 JS 对象,创建和操作的成本极低。
一个具体的映射例子:
<div id="app" class="container">
<span>Hello</span>
</div>
这个 HTML 片段,在 Vue 中会被编译成类似于下面的 VNode 树:
const vnode = {
type: 'div',
props: { id: 'app', class: 'container' },
children: [
{
type: 'span',
props: null,
children: 'Hello'
}
]
}
它就是一个普通的 JavaScript 数据,你可以在内存中随意复制、比较、修改,而不会触发任何浏览器重排或重绘。
虚拟 DOM 解决了什么实际问题
虚拟 DOM 的出现,核心是为了解耦 “你想让界面长什么样” 和 “浏览器如何高效更新界面” 这两个问题。
- 跨平台抽象
VNode 是对 UI 的纯数据描述,它不依赖浏览器环境。同样的 VNode 树,可以在浏览器、Node.js(服务端渲染)、原生移动端(如 Weex)中渲染出不同的“视图”。Vue 的渲染器(renderer)负责将 VNode 转化为具体平台的 UI,而核心 Diff 算法不需要改变。
- 批量更新,减少 DOM 操作
当数据连续变化时,Vue 并不会马上操作真实 DOM,而是先在内存中生成新的 VNode 树,与旧的 VNode 树进行同层对比(Diff),找出真正需要更新的最小节点集合,最后只对这些节点执行一次真实 DOM 操作。比如一个列表新增了一项,Vue 只会在合适的位置插入一个真实节点,而不会重新渲染整个列表。
- 可预测的渲染模型
虚拟 DOM 让组件的渲染输出变成了一个纯函数——给定相同的状态,会产生相同的 VNode 树。这让“组件的界面是什么”变成可预测的,也方便进行单元测试(可以直接断言 VNode 结构)和调试。
虚拟 DOM 不是银弹
需要明确的是,虚拟 DOM 并不是魔法的性能加速器。每次生成新的 VNode 树,并进行 Diff 对比,本身也需要 CPU 时间。当页面变化极小时,精心优化的直接 DOM 操作可能比虚拟 DOM 更快。但虚拟 DOM 的优势在于:它为开发者提供了一个足够快且心智负担低的通用方案,你不再需要针对每次数据更新手动选择最优的 DOM 操作策略,Vue 的渲染器已经为你做好了。
同时,Vue 3 通过编译阶段的静态提升与 PatchFlags,大幅减少了 Diff 时需要遍历的 VNode 数量,让虚拟 DOM 的性能瓶颈被进一步压低(详见 5.4 节)。这使得在绝大多数业务场景下,虚拟 DOM 的开销可以忽略不计。
一句话总结:虚拟 DOM 本质是一棵用 JavaScript 对象描述的“轻量 DOM 树”,它作为数据层与真实 DOM 之间的缓冲,让 Vue 能够高效、跨平台地实现声明式的 UI 更新。