Appearance
虚拟 DOM 与响应式:框架的"性能内功"
最后更新:2026-08-19
本主题要回答的问题
- 数据驱动(声明式)说"只管状态",可状态一变,DOM 怎么知道改哪?全改吗?
- 虚拟 DOM 到底"虚拟"在哪?它比直接操作真实 DOM 快在哪、慢在哪?
- React 的虚拟 DOM 与 Vue 的响应式是两条路,它们各自解决什么问题?
一、问题:声明式 UI 的性能难题
MVVM/数据驱动带来的自由是有代价的:既然开发者不写"如何更新 DOM",那么状态变化时必须有人负责把变化映射到 DOM。最朴素的做法是"全量重建 DOM"——但真实 DOM 的创建、布局、绘制成本高昂,一个 1000 节点的列表全量重建会明显卡顿。
框架需要回答:"状态变了"之后,如何以最小代价更新真实 DOM?
二、虚拟 DOM:把"对比"放到 JS 层做
React 的方案(2013):在 JS 内存里维护一棵描述 UI 的普通对象树(虚拟 DOM),状态变化后:

| 对比项 | 直接操作真实 DOM | 虚拟 DOM diff |
|---|---|---|
| 对比成本 | 真实 DOM 对比慢(含布局/样式) | JS 对象对比快(纯内存计算) |
| 更新方式 | 手动或全量重建 | 只把差异应用到真实 DOM |
| 心智模型 | 命令式 | 声明式(React 内部替你 diff) |
| 性能上限 | 高度依赖开发者水平 | 稳定,但对"超大列表"仍需优化 |
本质:虚拟 DOM 不是"比手写 DOM 操作快",而是把"从状态到 UI 的更新"这件事变得可自动、可预测、足够快——它用 JS 层的一次廉价 diff,换取真实 DOM 的少量昂贵操作。
三、Vue 的响应式:另一条路
Vue(2014)走的是响应式依赖追踪:状态被"劫持"(Vue 2 用 Object.defineProperty,Vue 3 用 Proxy),谁读取了某个状态,框架就记录"这个组件依赖这个状态";状态一变,只有依赖它的组件被标记并重新渲染,连 diff 都省到最小。
| 对比项 | React 虚拟 DOM | Vue 响应式 |
|---|---|---|
| 追踪粒度 | 组件级(diff 时才知道哪些变了) | 状态级(谁读谁被依赖) |
| 更新触发 | 每次 setState 后 diff 整棵组件树 | 只重渲染依赖该状态的组件 |
| 心智模型 | "UI = f(state)",state 不可变 | "状态可改,改动自动传播" |
| 代表实现 | React Reconciler + Fiber | Vue 3 Proxy + 依赖收集 |

四、性能边界与 Svelte 的激进方案
两条路线都有边界:
- React:组件树大时,diff 也有成本(Fiber 把 diff 切成可中断的小块解决卡顿);
- Vue:依赖追踪精细,但深层对象/复杂派生状态需要
computed等设计配合; - Svelte(2019):更进一步——干脆编译期把"声明式代码"编译成"精准命令式 DOM 操作",运行时连虚拟 DOM 都没有,"编译器替你写 jQuery"。
| 方案 | 更新机制 | 运行时开销 |
|---|---|---|
| React | 运行时 diff | 有(虚拟 DOM 对象) |
| Vue | 运行时响应式 | 有(Proxy 劫持) |
| Svelte | 编译期生成命令式代码 | 几乎为零 |
一句话总结
虚拟 DOM 与响应式的本质是同一件事的两条实现路径——用可预测的机制替开发者完成"状态 → DOM 的最小更新":React 在运行时 diff 虚拟树,Vue 在运行时追踪依赖,Svelte 则把这件事挪到编译期;框架之争的底层,比的是"状态同步"这件事由谁、在哪个阶段、花多大代价完成。