虚拟DOM是用JS对象精确映射真实DOM结构与行为的中间表示,必须支持1:1序列化为真实节点、与DOM生命周期对齐,并通过diff算法实现最小化更新。

虚拟 DOM 不是真实 DOM,但它必须能映射到真实 DOM
很多人一上来就背“虚拟 DOM 是用 JS 对象模拟 DOM 树”,这没错,但容易忽略关键约束:它必须能 1:1 可序列化为真实 DOM 节点。比如 document.createElement('div') 生成的节点有 nodeType、childNodes、getAttribute 等行为,而一个普通对象如 { tag: 'div', props: {}, children: [] } 没有这些能力——它只是描述,不是代理。
所以框架(React/Vue)的虚拟 DOM 实际是带类型和方法的结构体,不是裸对象。面试时如果被问“怎么实现”,别只画个 JSON 树,得说明它如何与真实 DOM 生命周期对齐(比如 mount 阶段调用 document.createElement,patch 阶段调用 el.setAttribute 或 el.textContent =)。
diff 算法不等于“逐节点递归比较”
真实框架的 diff 是有强假设和剪枝策略的,不是无脑树遍历。React 的 reconciler 假设同一层级组件 key 不重复,且默认把子节点当有序列表处理;Vue 2 的 patch 则在相同 tag 下优先比对属性,再递归子节点,遇到 key 不匹配直接放弃复用。
常见错误理解:
立即学习“前端免费学习笔记(深入)”;
- 认为 diff 会跨层级找相同节点(不会,React/Vue 都不做跨层移动优化)
- 以为加了
key就一定提升性能(key 写错或动态生成反而导致强制重渲染) - 忽略文本节点的特殊处理(比如
textContent变更不触发子树 diff,直接替换)
实操建议:写自定义渲染器时,先明确你的 diff 边界——是只比 props,还是也比事件监听器?是否支持 fragment?这些决定你 diff 函数的输入结构和退出条件。
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
为什么不能跳过虚拟 DOM 直接操作真实 DOM?
不是不能,而是成本不可控。重点不在“慢”,而在“不可预测的慢”。
比如连续 10 次 el.style.color = 'red' + el.className = 'active',浏览器可能触发 10 次回流;而虚拟 DOM 把这 10 次变更收集成一次 patch,最终只调一次 el.setAttribute('class', 'active') 和一次 el.style.cssText = 'color: red;'。
关键差异点:
- 真实 DOM 操作会立即触发 layout(重排)和 paint(重绘),而虚拟 DOM 的变更发生在内存,无副作用
- 框架可批量 flush(如 React 的
batchedUpdates),把多个 setState 合并成一次 diff - 服务端渲染(SSR)或跨平台(React Native)依赖虚拟 DOM 的中间表示,否则无法解耦平台逻辑
手写简易 virtual DOM 渲染器要注意的三个硬坑
很多面试者现场写个 h 函数 + render 就以为完成,但实际运行会崩:
-
h('div', { onClick: handler })中的事件名必须转成小写(onclick),且 handler 要绑定到真实 DOM 上,不能只存进 vnode —— 否则点击没响应 - 文本节点不能用
{ tag: '#text', text: 'hello' }表示,而要用document.createTextNode('hello')创建,否则后续 patch 无法正确替换 - 组件更新时,若新 vnode 的
key和旧 vnode 相同但 type 不同(比如从Button变成Input),必须先unmount再mount,不能复用 DOM 节点 —— 否则状态残留
最易被忽略的是:虚拟 DOM 的“虚拟”二字,只针对构建和 diff 阶段;一旦进入 patch,它必须精确控制真实 DOM 的每一个属性、子节点、事件监听器,否则就是空中楼阁。


















