很多人第一次听到“虚拟 DOM”时,会觉得它是个“多此一举”的东西:既然最终还是要操作真实 DOM,为什么要先在 JavaScript 对象上折腾一遍?但实际上,虚拟 DOM 解决的恰恰是“怎么高效、灵活地操作真实 DOM”的问题。它的核心价值可以概括为三点。
跨平台抽象:一次描述,多端渲染
虚拟 DOM 本质上是一个用 JavaScript 对象描述的界面树,它不依赖浏览器环境。比如一个简单的 <div>:
// 虚拟 DOM 节点(简化结构)
const vnode = {
tag: 'div',
props: { id: 'app' },
children: [
{ tag: 'span', props: null, children: 'Hello' }
]
}
这个对象与平台无关。同样的虚拟 DOM 树,在不同平台可以用不同的渲染器来转换成最终的视图:
- 在浏览器中,渲染器将虚拟 DOM 映射为真实 DOM 操作。
- 在 Weex 或 UniApp 中,渲染器将其映射为原生移动端组件。
- 在服务端渲染中,渲染器将其序列化为 HTML 字符串。
这意味着 Vue 的核心运行时和响应式系统是平台无关的,只需要替换渲染模块,就能让同一套组件代码运行在不同的终端上。Vue 3 的 createRenderer API 甚至允许开发者自定义渲染器,比如将虚拟 DOM 渲染到 Canvas、WebGL 或者 iOS/Android 原生视图。这种跨平台能力是直接操作真实 DOM 无法做到的。
批量更新:合并操作,减少重复渲染
在真实的交互场景中,同一时刻可能有多个数据变化:
data.name = 'Alice';
data.age = 30;
data.active = true;
如果每改一个数据就立刻去更新对应的 DOM 节点,浏览器会进行多次重排、重绘,性能消耗巨大且可能造成页面闪烁。虚拟 DOM 配合 Vue 的异步更新队列,会将多次数据变更对应的虚拟 DOM 更新全部收集起来,在一个异步任务(微任务)中一次性完成新旧虚拟 DOM 的比较(Diff),然后只对发生变化的几个真实 DOM 节点做一次性更新。这样,无论你在一帧里改了多少次数据,最终只产生一次 DOM 操作批次,渲染性能得到有效保障。
降低 DOM 操作开销:只改该改的,不改不该改的
真实 DOM 的访问和修改代价高昂——读取一个 offsetWidth 都可能触发浏览器回流。虚拟 DOM 将对比过程完全放在轻量的 JavaScript 对象上进行,然后计算出最小的必要 DOM 操作指令集。
举个例子,一个列表中有上千个 <li>,当你只修改了其中一条数据时:
- 传统手动拼接 HTML 的方案可能会
innerHTML全部重写,导致整个列表销毁再创建,丢失输入框焦点、滚动位置,且触发大量 DOM 操作。 - 虚拟 DOM 通过 Diff 算法定位到真正变化的那一个
<li>,然后仅对该节点执行一次textContent更新。其余节点完全复用,原封不动。
这种精确的差异更新,把 DOM 操作从“重刷”变成了“微调”,极大降低了浏览器的渲染负担。而且,虚拟 DOM 的差异计算本身很快,因为 Vue 的编译器在构建时就已经标记好了静态与动态节点,Diff 时直接跳过静态节点,只对比带 PatchFlag 的动态内容,进一步减少了对比范围。
一句话总结:虚拟 DOM 提供了一个轻量、平台无关的界面描述层,通过批量延迟更新和精准差异计算,让渲染既高效又灵活。你写的仍然是声明式的模板,而虚拟 DOM 在底层默默做了“如何省力地更新界面”的脏活累活。