虚拟dom的diff中的双端比较算法
双端比较算法是Vue中用于高效比较新旧VNode子节点的一种策略。该算法的核心思想是,通过从新旧VNode子节点的两端开始比较,逐步向中间靠拢,以找到最小的差异并据此更新DOM。以下是双端比较算法的大致流程:
- 初始化指针:设置四个指针,分别指向新旧VNode子节点的开始和结束位置。
- 首尾比较:首先比较新旧VNode子节点的首尾元素。如果首尾元素相同,则直接复用,并移动相应的指针。
- 交叉比较:如果首尾元素不同,则进行交叉比较,即比较旧节点的末尾和新节点的开头,以及旧节点的开头和新节点的末尾。
- 移动指针:根据比较结果,移动指针以缩小比较范围。如果找到匹配的节点,则复用该节点,并根据匹配情况调整指针位置。
- 结束条件:当任一节点的开始指针超过结束指针时,表明已经遍历完至少一个节点的所有子节点,此时结束比较。

这里以伪代码的形式进行展示,其中oldChildren是旧的虚拟DOM,newChildren是新的虚拟DOM,通过patch来更新真实的DOM结构
function diff(oldChildren, newChildren) {let oldStartIdx = 0 // 旧子节点起始指针let oldEndIdx = oldChildren.length - 1 // 旧子节点结束指针let newStartIdx = 0 // 新子节点起始指针let newEndIdx = newChildren.length - 1 // 新子节点结束指针while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {// 获取当前指针指向的节点let oldStartVNode = oldChildren[oldStartIdx]let oldEndVNode = oldChildren[oldEndIdx]let newStartVNode = newChildren[newStartIdx]let newEndVNode = newChildren[newEndIdx]// --- 四种比较场景 ---// 1. 旧头 vs 新头(复用)if (oldStartVNode.key === newStartVNode.key) {patch(oldStartVNode, newStartVNode) // 更新节点属性oldStartIdx++newStartIdx++}// 2. 旧尾 vs 新尾(复用)else if (oldEndVNode.key === newEndVNode.key) {patch(oldEndVNode, newEndVNode)oldEndIdx--newEndIdx--}// 3. 旧头 vs 新尾(移动:旧头移动到旧尾之后)else if (oldStartVNode.key === newEndVNode.key) {patch(oldStartVNode, newEndVNode)insert(oldStartVNode.el, parentEl, oldEndVNode.el.nextSibling) // DOM移动操作oldStartIdx++newEndIdx--}// 4. 旧尾 vs 新头(移动:旧尾移动到旧头之前)else if (oldEndVNode.key === newStartVNode.key) {patch(oldEndVNode, newStartVNode)insert(oldEndVNode.el, parentEl, oldStartVNode.el)oldEndIdx--newStartIdx++}// --- 如果四种场景都不匹配 ---else {// 尝试在旧子节点中查找与 newStartVNode 匹配的节点let foundIdx = oldChildren.findIndex(node => node.key === newStartVNode.key)if (foundIdx !== -1) {// 找到可复用的旧节点,移动它到旧头之前let foundVNode = oldChildren[foundIdx]patch(foundVNode, newStartVNode)insert(foundVNode.el, parentEl, oldStartVNode.el)oldChildren[foundIdx] = undefined // 标记该位置已处理} else {// 没有可复用节点,创建新节点插入到旧头之前createEl(newStartVNode)insert(newStartVNode.el, parentEl, oldStartVNode.el)}newStartIdx++}}// --- 处理剩余节点 ---// 1. 如果旧子节点遍历完毕,剩余新节点需要新增if (oldStartIdx > oldEndIdx && newStartIdx <= newEndIdx) {for (let i = newStartIdx; i <= newEndIdx; i++) {createEl(newChildren[i])insert(newChildren[i].el, parentEl, oldChildren[oldStartIdx]?.el || null)}}// 2. 如果新子节点遍历完毕,剩余旧节点需要删除else if (newStartIdx > newEndIdx && oldStartIdx <= oldEndIdx) {for (let i = oldStartIdx; i <= oldEndIdx; i++) {remove(oldChildren[i].el)}}
}
其中patch函数的核心作用是 复用已有的真实 DOM 节点,并用新虚拟节点(VNode)的属性更新对应的真实 DOM
问题1:patch 函数的具体更新逻辑
(1)更新属性(props)
新增/修改属性:将新 VNode 的 class、style、id、自定义属性等同步到真实 DOM。
删除旧属性:移除新 VNode 中不存在的旧属性。
// 伪代码示例:更新 class
if (newVNode.class !== oldVNode.class) {el.className = newVNode.class;
}
(2)更新事件监听器
绑定新事件:若新 VNode 有事件(如 @click),则绑定到真实 DOM。
解绑旧事件:若旧事件不存在于新 VNode 中,则移除。
// 伪代码示例:更新事件
if (newVNode.onClick !== oldVNode.onClick) {el.removeEventListener('click', oldVNode.onClick);el.addEventListener('click', newVNode.onClick);
}
(3)更新子节点
递归对比子节点,触发子树的 Diff 算法:
// 伪代码:递归更新子节点
if (newVNode.children && oldVNode.children) {updateChildren(el, oldVNode.children, newVNode.children);
}
(4)更新文本内容
若节点是文本节点,直接更新文本:
if (newVNode.text !== oldVNode.text) {el.textContent = newVNode.text;
}
示例说明
假设新旧虚拟节点如下:
// 旧虚拟节点
oldVNode = {tag: 'div',class: 'old-class',onClick: oldHandler,children: [A, B]
};// 新虚拟节点
newVNode = {tag: 'div',class: 'new-class',onClick: newHandler,children: [B, A, C]
};
patch 函数的操作步骤:
- 更新 class:将真实 DOM 的 class 从 old-class 改为 new-class。
- 更新事件:解绑旧的oldHandler,绑定新的 newHandler。
- 更新子节点:触发子节点的双端 Diff 算法,复用 B 和 A,新增 C,可能移动节点位置。
问题:在更新过程中旧节点数量是否变化?
旧虚拟节点(oldChildren)的数量不会改变,但真实 DOM 中子节点数量会增加:
(1)新旧虚拟节点的数量是固定的
旧节点列表(oldChildren):在 Diff 过程中是固定的,长度由初始渲染时决定。
新节点列表(newChildren):是目标结构,算法需将真实 DOM 更新到该结构。
无论是否插入新节点,oldChildren 和 newChildren 的虚拟节点数量是固定的,不会动态增减。
(2)真实 DOM 的子节点数量会暂时增加
插入新节点的本质:在真实 DOM 中新增了一个节点(newStartVNode.el)。
旧节点的存在性:未被复用的旧节点仍然存在于真实 DOM 中,直到被明确删除。
此时真实 DOM 的子节点数量为:
旧节点数量 + 新增节点数量 - 已删除的旧节点数量。
2. 示例场景
假设初始状态为(这里是虚拟DOM):
oldChildren: [A, B, C] // 旧节点数量为 3
newChildren: [D, A, B] // 新节点数量为 3
执行流程如下:
- 处理新头节点 D:
- 在 oldChildren 中未找到可复用的节点。 创建新节点 D,并插入到旧头节点 A 之前。
- 此时真实 DOM 结构为 [D, A, B, C](4 个节点)。
继续 Diff 后续节点:
- 新头指针后移(newStartIdx++),处理下一个新节点 A。 发现 A 可复用旧头节点,移动并更新指针。
- 最终清理未处理的旧节点: 新节点处理完毕后,剩余的旧节点 C 会被删除。 最终真实 DOM 结构:[D, A, B](与新节点数量一致)。
相关文章:
虚拟dom的diff中的双端比较算法
双端比较算法是Vue中用于高效比较新旧VNode子节点的一种策略。该算法的核心思想是,通过从新旧VNode子节点的两端开始比较,逐步向中间靠拢,以找到最小的差异并据此更新DOM。以下是双端比较算法的大致流程: 初始化指针&…...
# 如何确认elementary os (linux)使用的是Wayland而不是x11?
如何确认elementary os (linux)使用的是Wayland而不是x11? 文章目录 如何确认elementary os (linux)使用的是Wayland而不是x11?**方法 1:使用 loginctl 命令(systemd 系统࿰…...
VMware安装Windows server 2016
1、新建虚拟机,选择自定义模式 2、选择兼容性 4、命名虚拟机 5、固件类型 EFI 虚拟磁盘类型,不同电脑推荐的类型不同,用默认的就行 删除声卡和打印机 检查网络配置 选择本地的Windows server 2016的系统镜像,系统镜像可以去Window…...
K8s 1.27.1 实战系列(十)PV PVC
一、核心概念与关系 1、PV(Persistent Volume) PV 是集群中的持久化存储资源,由管理员预先创建并配置,独立于 Pod 生命周期。它抽象了底层存储(如 NFS、云存储等),定义存储容量、访问模式(如 ReadWriteOnce)、回收策略(Retain/Delete/Recycle)等属性。例如,一…...
HippoRAG 2 原理精读
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 整体流程离线索引阶段在线检索和问答阶段 总结 整体流程 从上图可以看出,整个流程分为两个阶段 1、离线索引阶段 2、在线检索和问答阶段 离线索引阶段…...
三:FFMPEG拉流读取模块的讲解
FFMPEG拉流读取模块在远程监控项目最核心的作用是读取UVC摄像头传输的H264码流,并对其码流进行帧的提取,提取完成之后则把数据传输到VDEC解码模块进行解码。而在我们这个项目中,UVC推流的功能由FFMPEG的命令完成。 FFMPEG拉流读取模块的API…...
linux makefile tutorial
一个makefile的教程,几个小时就能看完,对makefile有个总体加细节的系统了解,非常不错: Learn Makefiles With the tastiest examples 中文翻译版: 起步 - Makefile 教程 (gavinliu6.github.io) gcc官网手册&#x…...
【从零开始学习计算机科学】操作系统(五)处理器调度
【从零开始学习计算机科学】操作系统(五)处理器调度 处理器调度一些简单的短程调度算法的思路先来先服务(First-Come-First-Served,FCFS)优先级调度及其变种最短作业优先调度算法(SJF)--非抢占式最短作业优先调度算法(SJF)--抢占式最高响应比优先调度算法轮转调度算法…...
视觉图像处理
在MATLAB中进行视觉图像处理仿真通常涉及图像增强、滤波、分割、特征提取等操作。以下是一个分步指南和示例代码,帮助您快速入门: 1. MATLAB图像处理基础步骤 1.1 读取和显示图像 % 读取图像(替换为实际文件路径) img = imread(lena.jpg); % 显示原图 figure; subplot(2…...
从零开始设计一个完整的网站:HTML、CSS、PHP、MySQL 和 JavaScript 实战教程
前言 本文将从实战角度出发,带你一步步设计一个完整的网站。我们将从 静态网页 开始,然后加入 动态功能(使用 PHP),连接 数据库,最后加入 JavaScript 实现交互功能。通过这个教程,你将掌握一个…...
JavaScript(Web APIs)
这个阶段两天也能看完 目录 壹_DOM-获取元素 00、获取DOM元素(根据CS选择器来获取DOM元素) 01、修改元素内容 02、修改CSS 03、H5自定义属性 04、定时器 贰_DOM-事件基础 00、事件监听 01、事件类型 02、事件对象 03、环境对象 04、回调函数 叁_DOM-事…...
Global top sap abap 和deepseek对话,测试其abap推理能力
我提交给deepseek一段代码 FUNCTION zXXX_hr_pafm_pannnn_up. *"---------------------------------------------------------------------- *"*"Local Interface: *" IMPORTING *" VALUE(IS_PRELP) TYPE PRELP OPTIONAL *" VALUE(IV…...
Android DUKPT - 3DES
一、DUKPT概述 DUKPT 即Derived Unique Key Per Transaction(每个事务的派生唯一密钥)。ANSI X9.24规范定义的密钥管理体系,主要用于对称密钥加密场景(如MAC、PIN等敏感数据保护)。通过动态生成唯一交易密钥ÿ…...
机器学习数学基础:45.多重响应分析
多重响应分析超详细教程:手把手教你分析多选题数据 一、深入理解多重响应分析的背景 问卷调查中,问题分为单选题与多选题: 单选题:如“你的性别?1.男 2.女”,答题者仅选一个选项,分析时直接统…...
《苍穹外卖》SpringBoot后端开发项目核心知识点与常见问题整理(DAY1 to DAY3)
目录 一、在本地部署并启动Nginx服务1. 解压Nginx压缩包2. 启动Nginx服务3. 验证Nginx是否启动成功: 二、导入接口文档1. 黑马程序员提供的YApi平台2. YApi Pro平台3. 推荐工具:Apifox 三、Swagger1. 常用注解1.1 Api与ApiModel1.2 ApiModelProperty与Ap…...
企业安全—对数据和资产进行识别和分类
0x00 前言 针对数据和资产的保护刻不容缓,这个是每一个做企业安全建设不容放过的一环,那么在识别数据和资产已经对这些数据分类就是必须要了解和掌握的内容。 这里不仅是针对商业机密,还有用户数据,前者在于保护公司利益&#x…...
QT系列教程(20) Qt 项目视图便捷类
视频连接 https://www.bilibili.com/video/BV1XY41127t3/?vd_source8be9e83424c2ed2c9b2a3ed1d01385e9 Qt项目视图便捷类 Qt项目视图提供了一些便捷类,包括QListWidget, QTableWidget, QTreeWidget等。我们分别介绍这几个便捷类。 我们先创建一个Qt …...
Spring Boot 调用DeepSeek API的详细教程
目录 前置准备步骤1:创建Spring Boot项目步骤2:配置API参数步骤3:创建请求/响应DTO步骤4:实现API客户端步骤5:创建控制器步骤6:异常处理步骤7:测试验证单元测试示例Postman测试请求 常见问题排查…...
动态扩缩容引发的JVM堆内存震荡:从原理到实践的GC调优指南
目录 一、典型案例:系统发布后的GC雪崩事件 (一)故障现象 1. 刚刚启动时 GC 次数较多 2. 堆内存锯齿状波动 3. GC日志特征:Allocation Failure (二)问题定位 二、原理深度解析:JVM内存弹…...
AI智能眼镜主控芯片:技术演进与产业生态的深度解析
一、AI智能眼镜的技术挑战与主控芯片核心诉求 AI智能眼镜作为XR(扩展现实)技术的代表产品,其核心矛盾在于性能、功耗与体积的三角平衡。主控芯片作为设备的“大脑”,需在有限空间内实现复杂计算、多模态交互与全天候续航…...
微服务拆分-远程调用
我们在查询购物车列表的时候,它有一个需求,就是不仅仅要查出购物车当中的这些商品信息,同时还要去查到购物车当中这些商品的最新的价格和状态信息,跟购物车当中的快照进行一个对比,从而去提醒用户。 现在我们已经做了服…...
[网络爬虫] 动态网页抓取 — Selenium 介绍 环境配置
🌟想系统化学习爬虫技术?看看这个:[数据抓取] Python 网络爬虫 - 学习手册-CSDN博客 0x01:Selenium 工具介绍 Selenium 是一个开源的便携式自动化测试工具。它最初是为网站自动化测试而开发的,类似于我们玩游戏用的按…...
【RAGFlow】windows本地pycharm运行
原因 由于官方只提供了docker部署,基于开源代码需要实现自己内部得逻辑,所以需要本地pycharm能访问,且docker运行依赖得其余组件,均需要使用开发服务器得配置。 修改过程 安装python 项目依赖于Python 版本:>3.1…...
git子仓库管理的两种方式
在团队协作中选择使用 Git Submodule 还是 Git Subtree 取决于项目的需求和团队的工作方式。以下是两者的对比和适用场景分析,帮助你做出选择: Git Submodule 优点 独立性高 子模块是一个独立的仓库,拥有自己的提交历史和分支。这使得子模…...
树莓派5首次开机保姆级教程(无显示器通过VNC连接树莓派桌面)
第一次开机详细步骤 步骤一:树莓派系统烧录1 搜索打开烧录软件“Raspberry Pi Imager”2 选择合适的设备、系统、SD卡3 烧录配置选项 步骤二:SSH远程树莓派1 树莓派插电2 网络连接(有线或无线)3 确定树莓派IP地址 步骤三ÿ…...
html-表格标签
一、表格标签 1. 表格的主要作用 表格主要用于显示、展示数据,因为它可以让数据显示的非常的规整,可读性非常好。特别是后台展示数据 的时候,能够熟练运用表格就显得很重要。一个清爽简约的表格能够把繁杂的数据表现得很有条理。 总…...
大模型安全新范式:DeepSeek一体机内容安全卫士发布
2月以来,DeepSeek一体机几乎成为了政企市场AI消费的最强热点。 通过一体机的方式能够缩短大模型部署周期,深度结合业务场景,降低中小企业对于大模型的使用门槛。据不完全统计,已约有超过60家企业基于DeepSeek推出一体机产品。 但…...
数据分析绘制随时间顺序变化图加入线性趋势线——numpy库的polyfit计算一次多项式拟合
import pandas as pd import numpy as np import matplotlib.pyplot as plt# 导入数据 data pd.read_csv(rC:\Users\11712\notebooktrain1.csv)# 假设数据包含 date_time 和 speed 列 data[date_time] pd.to_datetime(data[date_time]) # 确保时间列是 datetime 类型 data.s…...
密闭空间可燃气体监测终端:守护城市命脉,智驭燃气安全!
近年来,陕西省高度重视燃气安全,出台了一系列政策文件,旨在全面加强城镇燃气安全监管,防范化解重大安全风险。2023年,陕西省安委会印发《全省城镇燃气安全专项整治工作方案》,明确要求聚焦燃气经营、输送配…...
阿里千问大模型(Qwen2.5-VL-7B-Instruct)部署
参考链接 知乎帖子 B站视频 huggingface 镜像网站(不太全,比如 Qwen/Qwen2.5-VL-7B-Instruct就没有) huggingface 5种下载方式汇总 通过huggingface-cli下载模型 不一样的部分是预训练权重的下载和demo 首先安装huggingface_hub pip insta…...
