虚拟 DOM —— 中间层的智慧
作真实 DOM 有多贵先看一段代码// 把一个 div 的背景色改成红色 document.getElementById(box).style.backgroundColor red你觉得这一行代码的执行成本是多少答案远比你想象的复杂1. JS 引擎找到 DOM 节点 2. 修改 DOM 节点的 style 属性 3. 浏览器标记这个节点需要重新计算样式Recalculate Style 4. 重新布局Layout/Reflow—— 可能影响周围元素的位置 5. 重新绘制Paint—— 把新的颜色画到屏幕上 6. 合成Composite—— 把各层合成最终画面修改一个属性可能触发整个渲染流水线。如果是 1000 个属性修改呢如果是添加、删除、移动几百个节点呢直接操作真实 DOM 就像在沙滩上写字——每一笔都很简单但改一个字就可能要抹平整个沙滩。虚拟 DOM 是什么虚拟 DOM 就是用普通的 JavaScript 对象来描述一个 DOM 节点。真实 DOMdiv idapp classcontainer pHello/p /div等价的虚拟 DOM{ tag: div, props: { id: app, class: container }, children: [ { tag: p, props: {}, children: [ { tag: undefined, text: Hello } // 文本节点 ] } ] }Vue 中把这个 JS 对象叫做VNodeVirtual Node。为什么不用真实 DOM要自己造一个用一张表来对比真实 DOM虚拟 DOM (VNode)本质C 实现的浏览器对象普通 JS 对象创建成本高创建几百个属性低就几个字段操作成本高可能触发回流低只是改 JS 对象跨平台只能在浏览器可以渲染到不同平台可控性浏览器说了算框架完全控制核心思想用廉价的 JS 对象操作代替昂贵的 DOM 操作。先在 JS 层面做完所有计算最后一次性、最少化地更新真实 DOM。打个比方装修房子你要重新装修一个房间有两种方式方式一直接施工直接操作 DOM把左边这面墙砸掉 → 工人开始砸 等等右边那面也砸 → 工人换位置砸 不对左边还是留着吧 → 工人每次指示都立刻执行改主意了就返工。工期长、成本高。方式二先在图纸上画虚拟 DOM在图纸上画一遍 → 对比旧图纸 → 标记出所有改动 → 一次施工完成先在纸上JS 内存把所有方案画好确认无误后列出最小改动清单一次性施工。虚拟 DOM 就是那张设计图纸。Diff 算法怎么找出最小改动现有一棵旧的 VNode 树和一棵新的 VNode 树怎么找出最少改动把两棵树完全比较的时间复杂度是O(n³)——对一棵有 1000 个节点的树来说这是 10 亿次比较不可接受。但前端有一个重要的观察大部分情况下跨层级的移动非常罕见。基于这个假设Vue和 React的 diff 算法做了一个简化只比较同一层级的节点。不同层级的直接替换不尝试移动到另一层。这样算法退化到O(n)即每个节点只比较一次。同层 Diff 的三个步骤Vue 的 diff 采用的是双端比较策略。以下以子节点数组的 diff 为例。假设旧子节点是[A, B, C, D]新子节点是[B, A, D, E]。步骤 1头头比较旧: [A, B, C, D] ↑ 新: [B, A, D, E] ↑ A ! B → 不匹配结束头头比较步骤 2尾尾比较旧: [A, B, C, D] ↑ 新: [B, A, D, E] ↑ D ! E → 不匹配结束尾尾比较步骤 3头尾交叉比较旧头 vs 新尾: A vs E → 不匹配 旧尾 vs 新头: D vs B → 不匹配此时四个指针都没匹配上说明需要更复杂的操作。Vue 会尝试在旧节点中查找新节点是否存在通过 key。如果设置了 keydiv v-foritem in list :keyitem.idkey 的作用就是给每个 VNode 一个唯一的身份标识让 diff 算法能识别出这个节点只是位置变了不是被删除重建了。旧: [{key:A}, {key:B}, {key:C}, {key:D}] 新: [{key:B}, {key:A}, {key:D}, {key:E}] 有 key 时 B 在旧节点中找到 → 移动位置即可 A 在旧节点中找到 → 移动位置即可 D 在旧节点中找到 → 移动位置即可 E 不在旧节点中 → 新建 无 key 时 可能把 B 当成了 A因为都是第一个位置 → 更新 A 的内容为 B而不是移动 → 效率低还可能导致状态丢失动手实现一个迷你 VNode DiffVNode 创建function createVNode(tag, props, children) { return { tag, props, children } } function h(tag, props, ...children) { return createVNode(tag, props, children.flat()) }将 VNode 渲染为真实 DOMfunction mount(vnode, container) { // 创建元素 const el document.createElement(vnode.tag) // 设置属性 if (vnode.props) { for (const key in vnode.props) { el.setAttribute(key, vnode.props[key]) } } // 处理子节点 if (vnode.children) { vnode.children.forEach(child { if (typeof child string) { el.appendChild(document.createTextNode(child)) } else { mount(child, el) // 递归挂载 } }) } container.appendChild(el) vnode.el el // 保存对真实 DOM 的引用 }Diff 和 Patchfunction patch(oldVNode, newVNode) { const el (newVNode.el oldVNode.el) // 1. 标签不同 → 直接替换 if (oldVNode.tag ! newVNode.tag) { const newEl document.createElement(newVNode.tag) el.parentNode.replaceChild(newEl, el) mount(newVNode, el.parentNode) return } // 2. 更新属性 // 移除旧属性 for (const key in oldVNode.props) { if (!(key in newVNode.props)) { el.removeAttribute(key) } } // 设置新属性 for (const key in newVNode.props) { if (oldVNode.props[key] ! newVNode.props[key]) { el.setAttribute(key, newVNode.props[key]) } } // 3. 更新子节点 const oldChildren oldVNode.children || [] const newChildren newVNode.children || [] const len Math.max(oldChildren.length, newChildren.length) for (let i 0; i len; i) { if (i oldChildren.length) { // 新节点直接挂载 mount(newChildren[i], el) } else if (i newChildren.length) { // 旧节点多余删除 el.removeChild(oldChildren[i].el) } else { // 都存在递归 patch if (typeof oldChildren[i] string typeof newChildren[i] string) { if (oldChildren[i] ! newChildren[i]) { el.childNodes[i].textContent newChildren[i] } } else { patch(oldChildren[i], newChildren[i]) } } } }上面是最简版本的实现省略了 key 的匹配逻辑但已经能说明 Diff 的核心思想同层比较最小化 DOM 操作。虚拟 DOM vs 直接操作 DOM到底谁更快这是一个经典争论。答案是一般情况下直接操作 DOM 可以比虚拟 DOM 更快。但虚拟 DOM 能让你写出的代码足够好、足够可维护同时性能足够快。场景直接操作 DOM虚拟 DOM单个更新✅ 更快 有 diff 开销批量更新 需要手动优化✅ 自动合并代码可维护性❌ 散落各处✅ 声明式跨平台❌ 仅浏览器✅ 可渲染到原生虚拟 DOM 不是性能的最优解而是开发体验 足够好的性能之间的最优平衡。总结为什么需要虚拟 DOM真实 DOM 操作成本高虚拟 DOM 在 JS 层完成计算最后一次性最少地更新真实 DOM。VNode用 JS 对象描述 DOM 节点创建和比较成本极低。Diff 算法同层比较O(n) 复杂度。双端比较 key 优化是最核心的策略。Key 的作用给节点唯一标识让 Diff 能区分移动和替换。性能本质虚拟 DOM 是足够快 足够好维护的平衡方案。