本节摘要:虚拟 DOM 是框架在内存里维护的一棵"轻量 DOM 树"。数据一变,框架先算新旧树差异,再只把最小的一组修正落回真实页面,从而避开"全量重建页面"的低效。理解 diff 与协调,是读懂 React 渲染、Vue 更新一切性能话题的前提。
本节阅读目标
阅读完本节,你应当能够:
浏览器里操作真实 DOM 本身并不算慢,慢的是"修改引发重排重绘"这类连锁反应,以及"为了改一个数把整块页面重建一遍"的浪费。早期把整个列表重新 innerHTML 一次,代价四舍五入等于重画一面墙,只为了改墙角落里一块砖。
虚拟 DOM 的思路是:先在内存里用普通对象描述一棵树(这棵树你想怎么改都便宜),算清楚"到底要改哪些地方",再一次性把最小修正在真实页面上落地。
一次状态更新对应三步:
状态变化 → 生成新虚拟DOM树 → diff新旧两棵树 → 收集最小修补 → 落回真实DOM
写出来是流水账,画成图更直观。下面这张图把"声明式"如何靠虚拟 DOM 落地讲透。

看一个 key 的直观例子。渲染一个 ['A','B','C'] 的列表,随后变成 ['C','A','B']——如果你给每个条目绑了稳定 key(比如 key={item}),diff 会发现"只是顺序变了、节点都还在",于是只做一个"移动"操作;如果没绑 key、用下标当 key,框架会认为"下标 0 从 A 变成了 C、下标 1 从 B 变成 A……",于是三个条目整列重渲染。前者是一处轻量移动,后者是三处重建,代价天差地别。更糟的是带输入框的行可能因此丢掉正在输入的内容,因为你把"同一逻辑条目"误判成了"不同条目"。这个细节在 2.3 的列表改造里会再次碰到。
由上面的例子能顺出一条使用纪律:key 要用"内容里稳定不变的那部分"来充当,比如条目本身的 id,而不是会变的 text、更不是数组下标。删掉一条、插入一条都会让下标整体错位,下标 key 于是成了破坏复用、诱发隐 bug 的源头。这也是很多性能调优和诡异状态问题的共同根因:表面报在 render,深挖下去全是 key 不稳定。
调试渲染问题时,先给代码"分诊"能省一半时间。下面几条直指"键用错了"的红线,命中任一就优先怀疑 key 而非别的机制:
item(对象字面量或字符串变量)当 key,或多条记录 key 冲突——身份识别崩溃,极易随机丢内容。遇到这些现象,逐个把 key 换成稳定 id 再看性能与状态是否恢复正常。绝大多数"一列表一更新就卡"的问题,到这里就水落石出了:不是虚拟 DOM 慢,而是我们给它的"身份线索"太糊,逼它把能复用的地方也当新建来重建。
把更新从"耗时的大动作"压成"精准的小动作",换来的是可预测、对性能友好的网页。而关键在于,这套协议是框架自动执行的——你只要声明式地描述 UI,效率由机制兜底。这也是 React 与 Vue 同路的地方,尽管内部细节不同(React 的 Fiber、Vue 的精细化 patch),对外的心智都是这一套。
有同学以为"虚拟 DOM 一定比直接操作快"。真相是:对小规模更新差异不明显,虚拟 DOM 的价值更多是"开发体验(对齐声明式)+ 规模化时的可控性能",而不是对角成绩。遇到真正的高频长列表,还需要配合其他优化手段,详见第 3、4 章相关篇幅。
虚拟 DOM 判差异的细节很多,但只要你记住两条朴素规则,绝大多数"为什么列表变了"的现象都能想明白。第一条:标签类型不同,直接整块替换,不再往下比——把一个 <div> 换成 <span>,带着它子树一起重建,除非你用了同 type 才能复用。第二条:同标签,逐属性、逐子节点比对——属性里变了的打上更新,子节点里有 key 就按 key 对齐,没 key 就按下标对位。这两条组合起来,就解释了为什么要用稳定 key:它把"按下标猜身份"升级成"按内容认身份",从而让"移动"而不是"重建"发生。
像任何优化一样,虚拟 DOM 也有它的边界。高度频密的像素级动画(每帧都改一堆节点)或成千上万行、又有大量动态改动的表格,单靠 diff 可能仍力不从心,这时需要配合使用不变量、只改需要动的局部、或用专门的渲染策略。理解这条边界,你才不会在遇到一次卡顿就全盘否定"声明式"——问题多半在于你没把"真正该动的那一处"从大整体里摘出来,而不是机制本身失效。
下一节从"树怎么更新"转向"谁该更新"——数据驱动与响应式系统,讲清楚 Vue 的依赖追踪和 React 的显式渲染是怎么分工的。