大白话虚拟 DOM 原理与 diff 算法的实现机制

ops/2025/3/4 3:41:05/

虚拟 DOM 原理

啥是虚拟 DOM

想象一下,我们要建一座房子,在真正动手盖之前,先在纸上画一个房子的模型,这个模型就相当于虚拟 DOM。在网页开发里,真实的 DOM 就像那座真正的房子,而虚拟 DOM 就是用 JavaScript 对象来描述真实 DOM 结构的一个“模型”。

为啥要用虚拟 DOM

直接操作真实 DOM 是很“贵”的,这里的“贵”指的是性能开销大。每次修改真实 DOM,浏览器都要重新计算样式、布局,然后再把新的界面绘制出来,这个过程很耗时。而虚拟 DOM 是在 JavaScript 里操作的,速度非常快。我们可以先在虚拟 DOM 上做各种修改,然后对比修改前后的虚拟 DOM,找出差异,最后只把这些差异应用到真实 DOM 上,这样就能减少对真实 DOM 的操作次数,提高性能。

虚拟 DOM 原理步骤
  1. 创建虚拟 DOM:用 JavaScript 对象来描述真实 DOM 的结构和属性。
  2. 修改虚拟 DOM:在 JavaScript 里对虚拟 DOM 对象进行修改,模拟页面的变化。
  3. 对比虚拟 DOM:通过 Diff 算法比较修改前后的虚拟 DOM,找出差异。
  4. 更新真实 DOM:把找出的差异应用到真实 DOM 上,只更新需要改变的部分。

Diff 算法的实现机制

啥是 Diff 算法

Diff 算法就是用来比较两个虚拟 DOM 树的差异的,就像有两个房子模型,我们要找出它们之间哪里不一样。

Diff 算法的比较策略
  • 同级比较:只比较同一层级的节点,不会跨层级比较。就像比较两个房子模型,先比较第一层的房间,再比较第二层的房间,不会把第一层的房间和第二层的房间混在一起比较。
  • 类型判断:如果两个节点的类型不同(比如一个是 <div>,一个是 <p>),就认为这是两个完全不同的节点,直接替换。
  • key 的使用:在列表中,给每个节点加上一个唯一的 key,这样 Diff 算法就能更准确地判断哪些节点是新增的、哪些是删除的、哪些是移动的。

代码示例

// 1. 创建虚拟 DOM 节点的函数
function createElement(type, props, ...children) {return {type, // 节点类型,如 'div', 'p' 等props: {...props,children: children.map(child => typeof child === 'object'? child : createTextElement(child))}};
}// 创建文本节点的函数
function createTextElement(text) {return {type: 'TEXT_ELEMENT',props: {nodeValue: text,children: []}};
}// 2. 将虚拟 DOM 转换为真实 DOM
function render(vdom, container) {const dom = vdom.type === 'TEXT_ELEMENT'? document.createTextNode(vdom.props.nodeValue): document.createElement(vdom.type);// 设置属性const isProperty = key => key!== 'children';Object.keys(vdom.props).filter(isProperty).forEach(name => {dom[name] = vdom.props[name];});// 递归渲染子节点vdom.props.children.forEach(child => {render(child, dom);});// 将生成的真实 DOM 插入到容器中container.appendChild(dom);
}// 3. 简单的 Diff 算法示例
function diff(oldVdom, newVdom, container) {if (!oldVdom) {// 如果旧的虚拟 DOM 不存在,直接创建新的真实 DOMrender(newVdom, container);} else if (!newVdom) {// 如果新的虚拟 DOM 不存在,删除旧的真实 DOMcontainer.removeChild(getDom(oldVdom));} else if (oldVdom.type!== newVdom.type) {// 如果节点类型不同,替换旧的真实 DOMconst newDom = createDom(newVdom);container.replaceChild(newDom, getDom(oldVdom));} else {// 如果节点类型相同,更新属性updateDomProperties(getDom(oldVdom), oldVdom.props, newVdom.props);// 递归比较子节点const maxLength = Math.max(oldVdom.props.children.length,newVdom.props.children.length);for (let i = 0; i < maxLength; i++) {diff(oldVdom.props.children[i],newVdom.props.children[i],getDom(oldVdom));}}
}// 获取虚拟 DOM 对应的真实 DOM
function getDom(vdom) {if (vdom._dom) {return vdom._dom;}return vdom;
}// 更新真实 DOM 的属性
function updateDomProperties(dom, oldProps, newProps) {// 删除旧属性const isEvent = name => name.startsWith('on');Object.keys(oldProps).filter(isEvent).forEach(name => {const eventType = name.toLowerCase().substring(2);dom.removeEventListener(eventType, oldProps[name]);});// 添加新属性Object.keys(newProps).filter(isEvent).forEach(name => {const eventType = name.toLowerCase().substring(2);dom.addEventListener(eventType, newProps[name]);});// 更新其他属性const isProperty = key => key!== 'children' &&!isEvent(key);Object.keys(oldProps).filter(isProperty).forEach(name => {if (!newProps[name]) {dom[name] = '';}});Object.keys(newProps).filter(isProperty).forEach(name => {dom[name] = newProps[name];});
}// 示例使用
const oldVdom = createElement('div', { id: 'old' }, '旧的内容');
const newVdom = createElement('div', { id: 'new' }, '新的内容');
const container = document.getElementById('root');// 首次渲染
render(oldVdom, container);// 模拟更新,使用 Diff 算法更新 DOM
diff(oldVdom, newVdom, container);

代码解释

  1. createElement 函数:用来创建虚拟 DOM 节点,接收节点类型、属性和子节点作为参数,返回一个 JavaScript 对象来描述这个节点。
  2. render 函数:将虚拟 DOM 转换为真实 DOM,并插入到指定的容器中。它会递归处理子节点,确保整个虚拟 DOM 树都被转换为真实 DOM。
  3. diff 函数:实现了简单的 Diff 算法,比较旧的虚拟 DOM 和新的虚拟 DOM,根据不同情况更新真实 DOM。
  4. getDom 函数:获取虚拟 DOM 对应的真实 DOM。
  5. updateDomProperties 函数:更新真实 DOM 的属性,包括事件监听器和其他属性。

通过这些步骤,我们就实现了虚拟 DOM 的创建、渲染和更新,利用 Diff 算法减少了对真实 DOM 的操作,提高了性能。

大白话用代码示例详细解释虚拟 DOM 原理

啥是虚拟 DOM

咱先说说啥是虚拟 DOM。在网页开发里,真实的 DOM 就像是一座真正的大楼,有各种各样的房间(元素)和装饰(属性)。直接去改动这座大楼可费劲了,每次改动都得重新规划、装修,很耗时间和精力。而虚拟 DOM 呢,就像是这座大楼的一个模型,用一些材料(JavaScript 对象)做出来的,在这个模型上做改动就容易多了,速度也快。改完模型后,我们再看看模型和原来的有啥不一样,只把这些不一样的地方用到真正的大楼上,这样就能减少对真大楼的改动次数,提高效率。

代码实现虚拟 DOM 原理

1. 创建虚拟 DOM 节点
// 创建虚拟 DOM 节点的函数
function createElement(type, props, ...children) {return {type, // 节点类型,比如 'div'、'p' 这些,就像大楼里不同类型的房间props: {...props,// 处理子节点,如果子节点是普通文本,就把它变成文本节点对象children: children.map(child => typeof child === 'object'? child : createTextElement(child))}};
}// 创建文本节点的辅助函数
function createTextElement(text) {return {type: 'TEXT_ELEMENT',props: {nodeValue: text, // 文本内容,就像房间里的标语children: []}};
}

解释:

  • createElement 函数就像是一个造房间的工人,它能根据你给的房间类型(type)、房间的装修(props)和里面的小房间(children)来创建一个房间模型(虚拟 DOM 节点)。
  • createTextElement 函数是专门造标语房间(文本节点)的,把你给的文本变成一个有特定格式的房间模型。
2. 把虚拟 DOM 变成真实 DOM
// 将虚拟 DOM 渲染为真实 DOM 的函数
function render(vdom, container) {// 如果是标语房间(文本节点),就创建一个真正的标语const dom = vdom.type === 'TEXT_ELEMENT'? document.createTextNode(vdom.props.nodeValue): document.createElement(vdom.type);// 给房间加上装修(设置属性)const isProperty = key => key!== 'children';Object.keys(vdom.props).filter(isProperty).forEach(name => {dom[name] = vdom.props[name];});// 把小房间(子节点)都放进大房间里,递归处理vdom.props.children.forEach(child => {render(child, dom);});// 把造好的房间放到大楼的指定位置(容器)container.appendChild(dom);
}

解释:

  • render 函数就像是一个建筑工人,它根据虚拟 DOM 这个房间模型,用真正的材料(document.createTextNodedocument.createElement)造出一个真实的房间。
  • 然后给这个房间装上和模型一样的装修(属性)。
  • 接着把模型里的小房间也都按照同样的方法造出来,放到大房间里。
  • 最后把这个造好的房间放到大楼的指定位置(容器)。
3. 模拟页面更新和重新渲染
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
</head><body><div id="root"></div><script>// 创建一个虚拟 DOM 节点,就像设计一个大楼的房间布局const vdom = createElement('div', { id: 'example' }, createElement('h1', null, 'Hello, Virtual DOM!'),createElement('p', null, 'This is a simple example.'));// 找到大楼的指定位置(容器)const container = document.getElementById('root');// 第一次把设计好的房间布局变成真实的房间render(vdom, container);// 模拟页面状态更新,设计一个新的房间布局const newVdom = createElement('div', { id: 'example' }, createElement('h1', null, 'Updated: Hello, Virtual DOM!'),createElement('p', null, 'This is an updated example.'));// 这里先简单处理,清空原来的房间,再把新的房间布局变成真实的房间container.innerHTML = '';render(newVdom, container);</script>
</body></html>

解释:

  • 先设计一个大楼的房间布局(创建虚拟 DOM),然后把这个布局变成真实的房间(渲染虚拟 DOM)。
  • 过了一段时间,想改变一下房间布局(模拟页面状态更新),就设计一个新的布局(新的虚拟 DOM)。
  • 这里简单处理,把原来的房间都拆了(清空容器),再按照新的布局造新的房间(重新渲染新的虚拟 DOM)。
4. 用 Diff 算法优化更新
// 简单的 Diff 算法实现
function diff(oldVdom, newVdom, container) {if (!oldVdom) {// 如果原来没有房间布局,就直接按照新的布局造房间render(newVdom, container);} else if (!newVdom) {// 如果新的布局没有了,就把原来的房间拆了container.removeChild(getDom(oldVdom));} else if (oldVdom.type!== newVdom.type) {// 如果房间类型不一样,就把原来的房间拆了,按照新的类型造房间const newDom = createDom(newVdom);container.replaceChild(newDom, getDom(oldVdom));} else {// 如果房间类型一样,就只更新房间的装修和里面的小房间updateDomProperties(getDom(oldVdom), oldVdom.props, newVdom.props);// 递归比较小房间的差异const maxLength = Math.max(oldVdom.props.children.length,newVdom.props.children.length);for (let i = 0; i < maxLength; i++) {diff(oldVdom.props.children[i],newVdom.props.children[i],getDom(oldVdom));}}
}// 获取虚拟 DOM 对应的真实 DOM
function getDom(vdom) {if (vdom._dom) {return vdom._dom;}return vdom;
}// 更新真实 DOM 的属性
function updateDomProperties(dom, oldProps, newProps) {// 把原来的装修(事件属性)都拆了const isEvent = name => name.startsWith('on');Object.keys(oldProps).filter(isEvent).forEach(name => {const eventType = name.toLowerCase().substring(2);dom.removeEventListener(eventType, oldProps[name]);});// 装上新的装修(事件属性)Object.keys(newProps).filter(isEvent).forEach(name => {const eventType = name.toLowerCase().substring(2);dom.addEventListener(eventType, newProps[name]);});// 更新其他装修(属性)const isProperty = key => key!== 'children' &&!isEvent(key);Object.keys(oldProps).filter(isProperty).forEach(name => {if (!newProps[name]) {dom[name] = '';}});Object.keys(newProps).filter(isProperty).forEach(name => {dom[name] = newProps[name];});
}// 创建真实 DOM 节点
function createDom(vdom) {const dom = vdom.type === 'TEXT_ELEMENT'? document.createTextNode(vdom.props.nodeValue): document.createElement(vdom.type);// 设置属性const isProperty = key => key!== 'children';Object.keys(vdom.props).filter(isProperty).forEach(name => {dom[name] = vdom.props[name];});// 递归渲染子节点vdom.props.children.forEach(child => {const childDom = createDom(child);dom.appendChild(childDom);});vdom._dom = dom;return dom;
}

解释:

  • diff 函数就像是一个装修监工,它会比较原来的房间布局(旧虚拟 DOM)和新的房间布局(新虚拟 DOM)。
    • 如果原来没有布局,就直接按照新的布局造房间。
    • 如果新的布局没有了,就把原来的房间拆了。
    • 如果房间类型不一样,就把原来的房间拆了,按照新的类型造房间。
    • 如果房间类型一样,就只更新房间的装修(属性)和里面的小房间。
  • getDom 函数是用来找到虚拟 DOM 对应的真实房间的。
  • updateDomProperties 函数是用来更新房间的装修(属性)的,包括事件属性和其他属性。
  • createDom 函数和前面的 render 函数类似,是用来根据虚拟 DOM 造真实房间的。
5. 使用 Diff 算法更新页面
// 使用 Diff 算法更新页面
diff(vdom, newVdom, container);

解释:
最后调用 diff 函数,让装修监工来比较新旧布局,只更新有变化的地方,而不是把整个大楼都拆了重新造,这样就能提高效率啦。

虚拟 DOM 中 diff 算法的具体步骤

整体概念

想象一下你有两个积木城堡模型,一个是旧的,一个是新的。Diff 算法就像是一个“找茬大师”,要找出这两个模型之间的不同之处,然后只对真实的城堡做那些有变化的修改,这样就能省不少力气。在虚拟 DOM 里,这两个模型就是新旧虚拟 DOM 树,真实的城堡就是真实的 DOM。

具体步骤

1. 开始比较(根节点比较)

Diff 算法会先从两个虚拟 DOM 树的根节点开始比较。这就好比“找茬大师”先看看两个积木城堡的最下面那层有没有不一样。

  • 节点类型不同:如果两个根节点的类型不一样,比如说旧的根节点是个 <div>,新的根节点变成了 <p>,那就相当于两个城堡最下面那层的形状完全不同。这时候,Diff 算法会直接把旧的根节点对应的真实 DOM 节点删掉,然后用新的根节点创建一个全新的真实 DOM 节点。就好像把旧城堡最下面那层拆了,重新按照新的形状搭一层。

  • 节点类型相同:要是两个根节点类型一样,比如都是 <div>,那就接着比较它们的属性和子节点。这就像两个城堡最下面那层形状一样,但还得看看这层的装饰(属性)和上面堆的小积木(子节点)有没有变化。

2. 比较属性

当两个节点类型相同时,Diff 算法会比较它们的属性。这就像检查两个形状相同的积木层,看看上面贴的贴纸、装的小窗户这些装饰是不是一样。

  • 属性新增或修改:如果新节点有一些属性是旧节点没有的,或者相同属性的值不一样了,Diff 算法就会更新真实 DOM 节点的属性。比如新的 <div> 节点多了一个 class="active" 的属性,或者某个属性的值从 red 变成了 blue,那就会在真实的 <div> 节点上加上或者修改这个属性。

  • 属性删除:要是旧节点有某个属性,新节点却没有了,Diff 算法会把真实 DOM 节点上对应的属性删掉。就像把旧城堡那层上的某个贴纸撕下来。

3. 比较子节点

比较完属性后,Diff 算法会开始比较两个节点的子节点。这就像看看两个积木层上面堆的小积木有啥不同。这里面有几种不同的情况:

  • 旧节点没有子节点,新节点有子节点:这就好比旧城堡那层上面啥都没堆,新城堡那层上面堆了好多小积木。Diff 算法会把新节点的子节点依次创建成真实 DOM 节点,然后添加到对应的真实 DOM 父节点下面。

  • 旧节点有子节点,新节点没有子节点:和上面相反,旧城堡那层上面有小积木,新城堡那层空了。Diff 算法会把旧节点对应的真实 DOM 节点下面的子节点都删掉。

  • 新旧节点都有子节点:这种情况比较复杂,Diff 算法会采用一些策略来高效地比较。常见的策略是使用 key 值。

    • 使用 key:在列表渲染的时候,每个子节点都有一个唯一的 key。Diff 算法会根据 key 来判断哪些子节点是新增的、哪些是删除的、哪些是移动的。比如旧的子节点列表是 [A, B, C],新的是 [D, A, B],通过 key 就能快速知道 C 被删掉了,D 是新增的。

    • 没有 key:如果没有 key,Diff 算法会按顺序依次比较每个子节点。这可能会导致一些不必要的操作,比如明明只是某个子节点的位置移动了,但因为没有 key算法可能会认为是删掉了旧节点,又重新创建了一个新节点。

4. 递归比较

对于每个子节点,Diff 算法会重复上面的步骤,也就是先比较节点类型,再比较属性,最后比较子节点。这就像“找茬大师”从城堡的最下面一层开始,一层一层往上找不同,每一层都仔细检查上面的小积木。通过这种递归的方式,Diff 算法会遍历整个虚拟 DOM 树,找出所有的差异。

5. 更新真实 DOM

当 Diff 算法找出所有的差异后,就会根据这些差异来更新真实的 DOM。它会把需要删除的节点从真实 DOM 中移除,把需要新增的节点添加进去,把需要修改属性的节点进行属性更新。这样,真实的 DOM 就和新的虚拟 DOM 树保持一致了,就像把积木城堡按照新的模型修改好了。

总结

Diff 算法通过从根节点开始,先比较节点类型,再比较属性和子节点,采用递归的方式遍历整个虚拟 DOM 树,找出新旧虚拟 DOM 树的差异,最后根据这些差异来更新真实 DOM。使用 key 值可以让 Diff 算法更高效地找出差异,避免不必要的操作,提高性能。


http://www.ppmy.cn/ops/162944.html

相关文章

Vue.js Vue 测试工具:Vue Test Utils 与 Jest

Vue.js Vue 测试工具&#xff1a;Vue Test Utils 与 Jest 在 Vue.js 的开发过程中&#xff0c;编写和执行测试是确保应用质量和稳定性的关键步骤。Vue Test Utils 和 Jest 是 Vue.js 官方推荐的测试工具&#xff0c;二者结合使用&#xff0c;可以高效地进行单元测试和集成测试…

【Linux vi文本编辑器使用指南】

Linux vi文本编辑器使用指南 一、模式切换二、启动与退出三、光标移动&#xff08;命令模式&#xff09;四、编辑文本五、查找与替换六、其他实用命令七、示例流程八、学习建议 Linux系统中的 vi&#xff08;及其增强版 vim&#xff09;是一款功能强大的文本编辑器&#xff0…

【Transformer模型学习】第三篇:位置编码

文章目录 0. 前言1. 为什么需要位置编码&#xff1f;2. 如何进行位置编码&#xff1f;3. 正弦和余弦位置编码4. 举个例子4.1 参数设置4.2 计算分母项4.3 计算位置编码4.4 位置编码矩阵 5. 相对位置信息6. 改进的位置编码方式——RoPE6.1 RoPE的核心思想6.2 RoPE的优势 7. 总结 …

Nginx系列05(负载均衡、动静分离)

目录 Nginx 负载均衡 Nginx 动静分离 Nginx 负载均衡 概念&#xff1a;负载均衡是一种将网络流量分摊到多个后端服务器&#xff08;节点&#xff09;上的技术&#xff0c;以提高系统的可用性、性能和可扩展性。通过负载均衡&#xff0c;Nginx 可以根据一定的算法将客户端请求…

基于SpringBoot+Vue的医院挂号管理系统+LW示例参考

系列文章目录 1.基于SSM的洗衣房管理系统原生微信小程序LW参考示例 2.基于SpringBoot的宠物摄影网站管理系统LW参考示例 3.基于SpringBootVue的企业人事管理系统LW参考示例 4.基于SSM的高校实验室管理系统LW参考示例 5.基于SpringBoot的二手数码回收系统原生微信小程序LW参考示…

React 高阶组件(HOC)

1.React 高阶组件&#xff08;HOC&#xff09; ****1. HOC&#xff08;高阶组件&#xff09;HOC (Higher - Order Component) 定义&#xff1a; 高阶组件是一个接收组件作为参数并返回新组件的函数&#xff0c;用于复用组件逻辑&#xff0c;遵循纯函数特性&#xff08;无副作用…

Composer如何通过GitHub Personal Access Token安装私有包:完整教程

使用Composer安全管理您的PHP私有依赖包 一、前言 在PHP开发中&#xff0c;我们经常需要将内部工具包托管为私有仓库。传统的账号密码验证方式存在安全隐患&#xff0c;而GitHub Personal Access Token&#xff08;PAT&#xff09;提供了一种更安全的鉴权方案。本文将通过4个…

【欢迎来到Git世界】Github入门

241227 241227 241227 Hello World 参考&#xff1a;Hello World - GitHub 文档. 1.创建存储库 r e p o s i t o r y repository repository&#xff08;含README.md&#xff09; 仓库名需与用户名一致。 选择公共。 选择使用Readme初始化此仓库。 2.何时用分支&#xf…