虚拟 DOM 与 Diff 算法
下面,我们来系统的梳理关于 虚拟 DOM 与 Diff 算法 的基本知识点:
一、虚拟 DOM 核心概念
1.1 什么是虚拟 DOM?
虚拟 DOM (Virtual DOM) 是内存中的轻量级 JavaScript 对象,用于描述真实 DOM 的结构和属性。它是真实 DOM 的抽象表示,充当应用状态和实际渲染之间的中间层。
// 真实 DOM 节点
<div class="container"><h1>Hello World</h1><p>Virtual DOM Example</p>
</div>// 对应的虚拟 DOM 对象
{type: 'div',props: { className: 'container' },children: [{type: 'h1',props: {},children: 'Hello World'},{type: 'p',props: {},children: 'Virtual DOM Example'}]
}
1.2 为什么需要虚拟 DOM?
问题 | 解决方案 | 优势 |
---|---|---|
直接操作 DOM 成本高 | 批量更新 | 减少重排重绘 |
手动更新复杂 | 声明式编程 | 简化开发 |
跨平台需求 | 统一抽象层 | 支持多平台 |
性能瓶颈 | 智能更新算法 | 优化渲染效率 |
状态管理困难 | 单向数据流 | 可预测状态 |
1.3 虚拟 DOM 工作流程
二、虚拟 DOM 实现原理
2.1 节点表示
class VNode {constructor(type, props, children) {this.type = type; // 节点类型:'div'、'span'等this.props = props; // 属性:{ id: 'app', class: 'container' }this.children = children; // 子节点数组或文本this.key = props.key; // 特殊标识,用于Diff优化this.dom = null; // 对应的真实DOM引用}
}
2.2 创建虚拟 DOM
function createElement(type, props, ...children) {// 扁平化子节点const flatChildren = children.flat().filter(child => child != null && child !== false);// 处理文本节点const normalizedChildren = flatChildren.map(child => typeof child === 'object' ? child : createTextNode(child));return new VNode(type,props || {},normalizedChildren);
}function createTextNode(text) {return new VNode('#text',{ nodeValue: text },[]);
}// 使用示例
const vdom = createElement('div', { id: 'app' },createElement('h1', null, 'Title'),createElement('p', { class: 'content' }, 'Content')
);
2.3 渲染虚拟 DOM 到真实 DOM
function render(vnode, container) {if (vnode.type === '#text') {return document.createTextNode(vnode.props.nodeValue);}const dom = document.createElement(vnode.type);// 设置属性Object.entries(vnode.props).forEach(([key, value]) => {if (key.startsWith('on') && typeof value === 'function') {// 处理事件const eventType = key.toLowerCase().substring(2);dom.addEventListener(eventType, value);} else if (key === 'className') {// 处理classdom.className = value;} else if (key !== 'key') {// 其他属性dom.setAttribute(key, value);}});// 递归渲染子节点vnode.children.forEach(child => {const childDom = render(child, dom);dom.appendChild(childDom);});// 保存真实DOM引用vnode.dom = dom;// 挂载到容器container.appendChild(dom);return dom;
}
三、Diff 算法核心原理
3.1 Diff 算法目标
- 最小变更集:找出最少必要的 DOM 操作
- 时间复杂度优化:从 O(n³) 优化到 O(n)
- 组件复用:最大化重用现有 DOM 节点
3.2 Diff 策略
- 同级比较:仅比较同层级节点,不跨层级
- 类型不同直接替换:节点类型不同则整个子树替换
- Key 优化:使用 key 标识可复用的节点
- 组件类型检查:相同组件类型复用实例
3.3 Diff 算法实现
function diff(oldVNode, newVNode) {// 1. 类型不同直接替换if (oldVNode.type !== newVNode.type) {return {type: 'REPLACE',oldNode: oldVNode,newNode: newVNode};}// 2. 文本节点更新if (oldVNode.type === '#text') {if (oldVNode.props.nodeValue !== newVNode.props.nodeValue) {return {type: 'TEXT_UPDATE',node: oldVNode,text: newVNode.props.nodeValue};}return null; // 无变化}// 3. 收集属性变更const propPatches = diffProps(oldVNode.props, newVNode.props);// 4. 比较子节点const childPatches = diffChildren(oldVNode.children, newVNode.children);// 5. 返回变更集if (propPatches.length > 0 || childPatches.length > 0) {return {type: 'UPDATE',node: oldVNode,props: propPatches,children: childPatches};}return null; // 无变化
}
四、关键 Diff 策略详解
4.1 属性比较 (diffProps)
function diffProps(oldProps, newProps) {const patches = [];// 检查更新的属性Object.keys(newProps).forEach(key => {if (oldProps[key] !== newProps[key]) {patches.push({type: 'SET_ATTR',key,value: newProps[key]});}});// 检查删除的属性Object.keys(oldProps).forEach(key => {if (!(key in newProps)) {patches.push({type: 'REMOVE_ATTR',key});}});return patches;
}
4.2 子节点比较 (diffChildren)
无 Key 比较(索引比较)
function diffChildren(oldChildren, newChildren) {const patches = [];const len = Math.max(oldChildren.length, newChildren.length);for (let i = 0; i < len; i++) {const oldChild = oldChildren[i];const newChild = newChildren[i];if (!oldChild && newChild) {// 新增节点patches.push({type: 'ADD',index: i,node: newChild});} else if (!newChild && oldChild) {// 删除节点patches.push({type: 'REMOVE',index: i,node: oldChild});} else {// 递归比较const childPatch = diff(oldChild, newChild);if (childPatch) {patches.push({...childPatch,index: i});}}}return patches;
}
有 Key 比较(高效复用)
function diffChildrenWithKey(oldChildren, newChildren) {const patches = [];// 1. 创建旧节点key映射const oldKeyMap = {};oldChildren.forEach((child, index) => {if (child.key) {oldKeyMap[child.key] = { index, node: child };}});// 2. 遍历新节点let lastIndex = 0;newChildren.forEach((newChild, newIndex) => {if (!newChild.key) {// 没有key按索引处理patches.push(...diffChildren(oldChildren, newChildren));return;}const oldItem = oldKeyMap[newChild.key];if (!oldItem) {// 新增节点patches.push({type: 'ADD',index: newIndex,node: newChild});} else {// 节点存在,检查移动或更新const { index: oldIndex, node: oldNode } = oldItem;// 递归比较子节点const childPatch = diff(oldNode, newChild);if (childPatch) {patches.push({...childPatch,index: oldIndex,newIndex});}// 节点位置移动if (oldIndex < lastIndex) {patches.push({type: 'MOVE',from: oldIndex,to: newIndex});}lastIndex = Math.max(lastIndex, oldIndex);// 删除已处理的旧节点delete oldKeyMap[newChild.key];}});// 3. 删除未使用的旧节点Object.values(oldKeyMap).forEach(({ index }) => {patches.push({type: 'REMOVE',index});});return patches;
}
五、批量更新与优化策略
5.1 更新队列与批量处理
class Updater {constructor() {this.patches = [];this.isBatching = false;this.queue = [];}batchUpdate(callback) {this.isBatching = true;callback();this.isBatching = false;this.processQueue();}addPatch(patch) {if (this.isBatching) {this.queue.push(patch);} else {this.applyPatch(patch);}}processQueue() {this.queue.forEach(patch => this.applyPatch(patch));this.queue = [];}applyPatch(patch) {// 根据patch类型更新真实DOMswitch (patch.type) {case 'REPLACE':this.replaceNode(patch.oldNode, patch.newNode);break;case 'TEXT_UPDATE':patch.node.dom.nodeValue = patch.text;break;case 'UPDATE':this.updateNode(patch);break;case 'ADD':this.addNode(patch);break;case 'REMOVE':this.removeNode(patch);break;case 'MOVE':this.moveNode(patch);break;}}replaceNode(oldNode, newNode) {const parent = oldNode.dom.parentNode;const newDom = render(newNode);parent.replaceChild(newDom, oldNode.dom);}// 其他更新方法...
}
5.2 关键优化技术
-
批量更新:
// React 中的 batchedUpdates import { unstable_batchedUpdates } from 'react-dom';unstable_batchedUpdates(() => {setCount(c => c + 1);setFlag(f => !f); });
-
时间切片:
// 使用 requestIdleCallback 分割任务 function performWork(deadline) {while (tasks.length > 0 && deadline.timeRemaining() > 0) {const task = tasks.shift();task();}if (tasks.length > 0) {requestIdleCallback(performWork);} }
-
惰性更新:
// Vue 3 的 effect 调度 watch(effect, {scheduler: (job) => {queueJob(job); // 加入队列queueFlush(); // 延迟刷新} })
六、框架实现差异
6.1 React Diff 算法特点
- 双指针策略:同时遍历新旧子节点
- Key 重要性:Key 是节点复用的关键标识
- Fiber 架构:增量渲染和任务分割
- 组件类型优先:相同类型组件复用
- 子树重用策略:DOM 结构相似时复用子树
6.2 Vue Diff 算法特点
- 双端比较:同时从新旧列表两端开始比较
- 原地复用:最大化复用现有 DOM 节点
- 最长稳定子序列:减少不必要的移动
- 组件级别复用:相同组件类型复用实例
- 静态节点优化:跳过静态子树比较
6.3 双端比较算法示例
function vueDiff(oldChildren, newChildren) {let oldStartIdx = 0;let oldEndIdx = oldChildren.length - 1;let newStartIdx = 0;let newEndIdx = newChildren.length - 1;while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {// 1. 头头比较if (isSameNode(oldChildren[oldStartIdx], newChildren[newStartIdx])) {patchNode(oldChildren[oldStartIdx], newChildren[newStartIdx]);oldStartIdx++;newStartIdx++;continue;}// 2. 尾尾比较if (isSameNode(oldChildren[oldEndIdx], newChildren[newEndIdx])) {patchNode(oldChildren[oldEndIdx], newChildren[newEndIdx]);oldEndIdx--;newEndIdx--;continue;}// 3. 头尾比较if (isSameNode(oldChildren[oldStartIdx], newChildren[newEndIdx])) {patchNode(oldChildren[oldStartIdx], newChildren[newEndIdx]);moveNode(oldChildren[oldStartIdx], oldChildren[oldEndIdx].dom.nextSibling);oldStartIdx++;newEndIdx--;continue;}// 4. 尾头比较if (isSameNode(oldChildren[oldEndIdx], newChildren[newStartIdx])) {patchNode(oldChildren[oldEndIdx], newChildren[newStartIdx]);moveNode(oldChildren[oldEndIdx], oldChildren[oldStartIdx].dom);oldEndIdx--;newStartIdx++;continue;}// 5. 查找可复用节点const newKeyMap = createKeyMap(newChildren, newStartIdx, newEndIdx);const oldKey = oldChildren[oldStartIdx].key;const newIndex = newKeyMap[oldKey];if (newIndex !== undefined) {const moveNode = oldChildren[oldStartIdx];const targetNode = newChildren[newIndex];patchNode(moveNode, targetNode);moveNode(moveNode, oldChildren[oldStartIdx].dom);newChildren[newIndex] = undefined; // 标记已处理oldStartIdx++;} else {// 新增节点createNewNode(newChildren[newStartIdx], oldChildren[oldStartIdx].dom);newStartIdx++;}}// 处理剩余节点...
}
七、性能优化与最佳实践
7.1 开发者优化建议
-
合理使用 Key:
// 正确:唯一稳定ID {items.map(item => (<Item key={item.id} data={item} /> ))}// 错误:索引作为Key {items.map((item, index) => (<Item key={index} data={item} /> ))}
-
避免不必要渲染:
// React.memo / Vue 的 shallowRef const MemoComponent = React.memo(MyComponent);// 组件内部优化 function MyComponent(props) {const heavyComputation = useMemo(() => compute(props.data), [props.data]); }
-
组件设计原则:
- 保持组件小型化
- 分离容器组件与展示组件
- 使用状态提升避免深层传递
- 合理划分组件边界
7.2 框架优化技术
技术 | React | Vue | 效果 |
---|---|---|---|
子树跳过 | shouldComponentUpdate | v-once | 减少比较范围 |
时间切片 | Concurrent Mode | Suspense | 避免主线程阻塞 |
惰性加载 | React.lazy | defineAsyncComponent | 减少初始负载 |
虚拟列表 | react-window | vue-virtual-scroller | 优化长列表 |
编译优化 | Prepack | Vue 模板编译 | 减少运行时开销 |
八、实际案例分析
8.1 列表重排序
// 旧列表
[{ id: 1, content: 'A' },{ id: 2, content: 'B' },{ id: 3, content: 'C' }
]// 新列表
[{ id: 3, content: 'C' },{ id: 1, content: 'A' },{ id: 2, content: 'B' }
]// Diff过程:
1. 识别相同ID的节点
2. 移动节点而非重新创建
3. 执行最少DOM操作(移动节点位置)
8.2 节点类型变更
// 旧
<div><Counter /><p>Content</p>
</div>// 新
<div><p>New Content</p><NewComponent />
</div>// Diff过程:
1. 根节点相同(div)
2. 第一个子节点类型不同(Counter → p)
3. 替换整个Counter子树
4. 比较第二个子节点(p → NewComponent)
5. 替换整个p子树