Skip to main content

虚拟 DOM 与 Diff

为什么需要虚拟 DOM

  • JS 对象描述真实 DOM,操作内存对象比直接频繁操作真实 DOM 更快
  • 通过 diff 找出最小变更,批量更新,减少回流重绘
  • 跨平台基础(同一份 vnode 可渲染到 DOM、Canvas、原生端)

vnode 与 render

// h / createElement:生成虚拟节点
function h(tag, props = {}, children = []) {
return { tag, props, children };
}

// vnode -> 真实 DOM
function render(vnode) {
// 文本节点
if (typeof vnode === "string" || typeof vnode === "number") {
return document.createTextNode(String(vnode));
}
const el = document.createElement(vnode.tag);
// 属性
Object.entries(vnode.props).forEach(([key, value]) => {
el.setAttribute(key, value);
});
// 递归渲染子节点
vnode.children.forEach((child) => {
el.appendChild(render(child));
});
return el;
}

const vnode = h("ul", { class: "list" }, [
h("li", {}, ["a"]),
h("li", {}, ["b"]),
]);
document.body.appendChild(render(vnode));

Diff 的核心策略

主流框架(React/Vue)的 diff 都基于三个前提把复杂度从 O(n³) 降到 O(n)

  1. 同层比较:只比较同一层级节点,不跨层移动;跨层直接「删除旧 + 新建」。
  2. 类型不同直接替换tag(或组件类型)不同,整棵子树重建。
  3. key 优化列表:同层列表通过 key 复用节点,避免「整体重排」,这也是 v-for/map 要写 key 的原因。

简化版 patch

function patch(oldVnode, newVnode, parent) {
// 删除
if (newVnode == null) {
parent.removeChild(oldVnode.el);
return;
}
// 文本节点
if (typeof newVnode === "string") {
if (oldVnode !== newVnode) {
parent.textContent = newVnode;
}
return;
}
// 类型不同:整体替换
if (oldVnode.tag !== newVnode.tag) {
const newEl = render(newVnode);
parent.replaceChild(newEl, oldVnode.el);
return;
}
// 类型相同:复用元素,更新属性 + 递归 diff 子节点
const el = (newVnode.el = oldVnode.el);
updateProps(el, oldVnode.props, newVnode.props);
diffChildren(el, oldVnode.children, newVnode.children);
}

常见追问

  • React Fiber 解决了什么? 把递归 diff 拆成可中断的小任务,避免长任务阻塞主线程(时间分片)。
  • Vue3 的 diff 优化? 编译期标记静态节点(静态提升)、PatchFlag 只 diff 动态部分、最长递增子序列优化列表移动。
  • 为什么 key 不能用 index? 列表增删/排序时 index 会变,导致复用错位、状态错乱。