当前位置: 首页 > news >正文

虚拟 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
与旧虚拟DOM对比Diff
计算最小变更集
批量更新真实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 策略

  1. 同级比较:仅比较同层级节点,不跨层级
  2. 类型不同直接替换:节点类型不同则整个子树替换
  3. Key 优化:使用 key 标识可复用的节点
  4. 组件类型检查:相同组件类型复用实例
开始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 关键优化技术

  1. 批量更新

    // React 中的 batchedUpdates
    import { unstable_batchedUpdates } from 'react-dom';unstable_batchedUpdates(() => {setCount(c => c + 1);setFlag(f => !f);
    });
    
  2. 时间切片

    // 使用 requestIdleCallback 分割任务
    function performWork(deadline) {while (tasks.length > 0 && deadline.timeRemaining() > 0) {const task = tasks.shift();task();}if (tasks.length > 0) {requestIdleCallback(performWork);}
    }
    
  3. 惰性更新

    // Vue 3 的 effect 调度
    watch(effect, {scheduler: (job) => {queueJob(job); // 加入队列queueFlush();  // 延迟刷新}
    })
    

六、框架实现差异

6.1 React Diff 算法特点

  1. 双指针策略:同时遍历新旧子节点
  2. Key 重要性:Key 是节点复用的关键标识
  3. Fiber 架构:增量渲染和任务分割
  4. 组件类型优先:相同类型组件复用
  5. 子树重用策略:DOM 结构相似时复用子树

6.2 Vue Diff 算法特点

  1. 双端比较:同时从新旧列表两端开始比较
  2. 原地复用:最大化复用现有 DOM 节点
  3. 最长稳定子序列:减少不必要的移动
  4. 组件级别复用:相同组件类型复用实例
  5. 静态节点优化:跳过静态子树比较

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 开发者优化建议

  1. 合理使用 Key

    // 正确:唯一稳定ID
    {items.map(item => (<Item key={item.id} data={item} />
    ))}// 错误:索引作为Key
    {items.map((item, index) => (<Item key={index} data={item} />
    ))}
    
  2. 避免不必要渲染

    // React.memo / Vue 的 shallowRef
    const MemoComponent = React.memo(MyComponent);// 组件内部优化
    function MyComponent(props) {const heavyComputation = useMemo(() => compute(props.data), [props.data]);
    }
    
  3. 组件设计原则

    • 保持组件小型化
    • 分离容器组件与展示组件
    • 使用状态提升避免深层传递
    • 合理划分组件边界

7.2 框架优化技术

技术ReactVue效果
子树跳过shouldComponentUpdatev-once减少比较范围
时间切片Concurrent ModeSuspense避免主线程阻塞
惰性加载React.lazydefineAsyncComponent减少初始负载
虚拟列表react-windowvue-virtual-scroller优化长列表
编译优化PrepackVue 模板编译减少运行时开销

八、实际案例分析

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子树
http://www.lryc.cn/news/576548.html

相关文章:

  • c++学习(五、函数高级)
  • 【AI智能体】Dify 核心组件从使用到实战操作详解
  • 设计模式-代理模式、装饰者模式
  • 【Java--SQL】${}与#{}区别和危害
  • git使用详解和示例
  • ByteMD+CozeAPI+Coze平台Agent+Next搭建AI辅助博客撰写平台(逻辑清楚,推荐!)
  • epitope3D: 精准预测蛋白表面的“抗原决定簇”
  • ABP VNext + 多数据库混合:SQL Server+PostgreSQL+MySQL
  • 【分布式机架感知】分布式机架感知能力的主流存储系统与数据库软件
  • 安卓应用启动页全版本兼容实战:从传统方案到Android 12+ SplashScreen API最佳实践
  • FPGA产品
  • 关于ubuntu 20.04系统安装分区和重复登录无法加载桌面的问题解决
  • KS值:风控模型的“风险照妖镜”
  • 北大肖臻《区块链技术与应用》学习笔记
  • 趣味数据结构之——数组
  • 给定一个整型矩阵map,求最大的矩形区域为1的数量
  • SRS WebRTC 入门
  • 【大模型】Query 改写常见Prompt 模板
  • 第27篇:SELinux安全增强机制深度解析与OpenEuler实践指南
  • uni-app项目实战笔记26--uniapp实现富文本展示
  • 【Actix Web 精要】Rust Web 服务开发核心技术与实战指南
  • [Java 基础]算法
  • 【AI实践】Mac一天熟悉AI模型智能体应用(百炼版)
  • nginx基本使用 linux(mac下的)
  • 【HarmonyOS Next之旅】DevEco Studio使用指南(三十八) -> 构建HAR
  • 编译安装交叉工具链 riscv-gnu-toolchain
  • RabbitMQ-基础篇
  • FreeSWITCH配置文件解析(2) dialplan 拨号计划中xml 的action解析
  • 1.1 基于Icarus Verilog、ModelSim和Vivado对蜂鸟E203处理器进行仿真
  • 学习使用dotnet-dump工具分析.net内存转储文件(2)