【Vue.js设计与实现】第三篇第11章:渲染器-快速 Diff 算法-阅读笔记
文章目录
- 11.1 相同的前置元素和后置元素
- 11.2 判断是否需要进行 DOM 移动操作
- 11.3 如何移动元素
- 11.4 总结
系列目录:【Vue.js设计与实现】阅读笔记目录
非常快的Diff算法。
11.1 相同的前置元素和后置元素
不同于简单 Diff 算法和双端 Diff 算法,快速 Diff 算法包含预处理步骤,这其实是借鉴了纯文本 Diff 算法的思路。
首先,进行全等比较,若全等就不用Diff了:
if(text1 === text2) return
再进行预处理,处理相同的前缀和后缀,如图:


const patchKeyedChildren = (n1, n2, container) => {const newChildren = n2.children;const oldChildren = n1.children;// 处理相同前置节点,j指向新旧两组子节点的开头let j = 0;let oldVNode = oldChildren[j];let newVNode = newChildren[j];// 直到遇到不同key值节点为止while (oldVNode.key === newVNode.key) {patch(oldVNode, newVNode, container);j++;oldVNode = oldChildren[j];newVNode = newChildren[j];}// 处理相同后置节点let oldEnd = oldChildren.length - 1;let newEnd = newChildren.length - 1;oldVNode = oldChildren[oldEnd];newVNode = newChildren[newEnd];while (oldVNode.key === newVNode.key) {patch(oldVNode, newVNode, container);oldEnd--;newEnd--;oldVNode = oldChildren[oldEnd];newVNode = newChildren[newEnd];}
};
预处理相同前后缀后:

可以发现,还遗留了一个未处理的p-4节点。我们可以很容易看出来这是一个新增节点,但是,如何得出这是新增节点的结论?
我们需要观察三个索引j、newEnd、oldEnd之间的关系:
- 条件1
oldEnd<j成立,说明在预处理过程中,所有旧节点都处理完毕了(开头的j>结尾的oldEnd) - 条件2
newEnd>=j成立,说明在预处理后,新节点中,有未处理的节点,即新增节点
因此,索引值在j和newEnd之间的节点都要作为新节点挂载:
// 预处理完毕// 挂载新节点
if (j > oldEnd && j <= newEnd) {const anchorIndex = newEnd + 1;const anchor =anchorIndex < newChildren.length? newChildren[anchorIndex].el: null;while (j <= newEnd) {patch(null, newChildren[j++], container, anchor);}
}
以上是新增节点的情况,接下来是删除节点的情况。

条件是:j > newEnd && j <= oldEnd
要卸载j-oldEnd的节点(由图知,从上到下是从小到大)
// 预处理完毕// 挂载新节点
if (j > oldEnd && j <= newEnd) {const anchorIndex = newEnd + 1;const anchor =anchorIndex < newChildren.length? newChildren[anchorIndex].el: null;while (j <= newEnd) {patch(null, newChildren[j++], container, anchor);}
} else if (j > newEnd && j <= oldEnd) {// 卸载旧节点while (j <= oldEnd) {unmount(oldChildren[j++]);}
}
11.2 判断是否需要进行 DOM 移动操作
前面的例子都是理想状况:去掉前后缀后,只需要简单的新增/移除。实际上,还会出现不理想的情况,如:

处理完前后缀后,还需要更多的处理。
其实,不管是简单Diff、双端Diff,还是快速Diff,他们的处理规则都相同:
- 判断是否有节点需要移动,以及如何移动
- 找出那些需要被添加或移除的节点
接下来我们将判断哪些节点要移动,以及如何移动。处理完前后缀后,我们会发现j、newEnd和oldEnd不满足上述的两种条件(新增/移除):
j > oldEnd && j <= newEndj > newEnd && j <= oldEnd
因此,我们需要新增一个else来处理这种情况。
首先,我们需要构造一个数组source,它的长度等于新的一组子节点在经过预处理之后剩余未处理节点的数量,并且 source 中每个元素的初始值都是 -1。
source 数组将用来存储新的一组子节点中的节点在旧的一组子节点中的位置索引,后面将会使用它计算出一个最长递增子序列,并用于辅助完成 DOM 移动的操作:

else {// 构造source数组// 新的一组子节点中剩余未处理节点的数量const count = newEnd - j + 1;const source = new Array(count);source.fill(-1);// source节点存储新节点在旧节点的索引const oldStart = j;const newStart = j;for (let i = oldStart; i <= oldEnd; i++) {const oldVNode = oldChildren[i];// 找到具有相同key的可复用节点for (let k = newStart; k <= newEnd; k++) {const newVNode = newChildren[k];if (oldVNode.key === newVNode.key) {patch(oldVNode, newVNode, container);source[k - newStart] = i;}}}
}
source数组如下:

这里需要注意的是,由于数组
source的索引是从 0 开始的,而未处理节点的索引未必从 0 开始,所以在填充数组时需要使用表达式k - newStart的值作为数组的索引值。外层循环的变量 i 就是当前节点在旧的一组子节点中的位置索引,因此直接将变量 i 的值赋给source[k - newStart]即可。
相当于都偏移 k - newStart。
不过,上述代码是双层嵌套的循环,复杂度可能太高了,我们得优化一下:我们可以为新的一组子节点构建一张索引表,用来存储节点的 key 和节点位置索引之间的映射。

这样就不用循环寻找了,直接在索引表新节点 key->i中里拿。
// 构建索引表:新节点 key->i
const keyIndex = {};
for (let i = newStart; i <= newEnd; i++) {keyIndex[newChildren[i].key] = i;
}
双层循环就可以改为一层循环:
若k存在,说明旧节点在新节点中存在,可以复用,否则卸载。
// 构建索引表:新节点 key->i
const keyIndex = {};
for (let i = newStart; i <= newEnd; i++) {keyIndex[newChildren[i].key] = i;
}for (let i = oldStart; i <= oldEnd; i++) {const oldVNode = oldChildren[i];const k = keyIndex[oldVNode.key];if (typeof k !== "undefined") {newVNode = newChildren[k];patch(oldVNode, newVNode, container);source[k - newStart] = i;} else {// 不存在,说明旧节点在新节点中不存在,卸载unmount(oldVNode);}
}
接下来我们将判断是否需要移动,与简单Diff相同,找递增的索引,递增的索引不需要移动。 否则需要,新增moved 和pos :
let moved = false;
let pos = 0;const keyIndex = {};
for (let i = newStart; i <= newEnd; i++) {keyIndex[newChildren[i].key] = i;
}for (let i = oldStart; i <= oldEnd; i++) {const oldVNode = oldChildren[i];const k = keyIndex[oldVNode.key];if (typeof k !== "undefined") {newVNode = newChildren[k];patch(oldVNode, newVNode, container);source[k - newStart] = i;// 判断节点是否需要移动:找递增的索引,若存在小于递减的索引,说明要移动if (k < pos) {moved = true;} else {pos = k;}} else {unmount(oldVNode);}
}
除此之外,我们还需要一个数量标识,代表已经更新过的节点数量。我们知道,已经更新过的节点数量应该小于新的一组子节点中需要更新的节点数量。一旦前者超过后者,则说明有多余的节点,我们应该将它们卸载。
count就是需要更新的节点数量。新增一个patched表示已更新过的节点数量:
// 记录更新过的节点数量
let patched = 0;for (let i = oldStart; i <= oldEnd; i++) {const oldVNode = oldChildren[i];// 如果更新过的节点数量小于需要更新的节点数量,则更新if (patched <= count) {const k = keyIndex[oldVNode.key];if (typeof k !== "undefined") {newVNode = newChildren[k];patch(oldVNode, newVNode, container);source[k - newStart] = i;patched++;if (k < pos) {moved = true;} else {pos = k;}} else {unmount(oldVNode);}} else {// 如果更新过的节点数量大于需要更新的节点数量,则卸载多余节点unmount(oldVNode);}
}
完整代码:
const patchKeyedChildren = (n1, n2, container) => {const newChildren = n2.children;const oldChildren = n1.children;// 处理相同前置节点,j指向新旧两组子节点的开头let j = 0;let oldVNode = oldChildren[j];let newVNode = newChildren[j];// 直到遇到不同key值节点为止while (oldVNode.key === newVNode.key) {patch(oldVNode, newVNode, container);j++;oldVNode = oldChildren[j];newVNode = newChildren[j];}// 处理相同后置节点let oldEnd = oldChildren.length - 1;let newEnd = newChildren.length - 1;oldVNode = oldChildren[oldEnd];newVNode = newChildren[newEnd];while (oldVNode.key === newVNode.key) {patch(oldVNode, newVNode, container);oldEnd--;newEnd--;oldVNode = oldChildren[oldEnd];newVNode = newChildren[newEnd];}// 预处理完毕// 挂载新节点if (j > oldEnd && j <= newEnd) {const anchorIndex = newEnd + 1;const anchor =anchorIndex < newChildren.length? newChildren[anchorIndex].el: null;while (j <= newEnd) {patch(null, newChildren[j++], container, anchor);}} else if (j > newEnd && j <= oldEnd) {// 卸载旧节点while (j <= oldEnd) {unmount(oldChildren[j++]);}} else {// 构造source数组// 新的一组子节点中剩余未处理节点的数量const count = newEnd - j + 1;const source = new Array(count);source.fill(-1);const oldStart = j;const newStart = j;let moved = false;let pos = 0;const keyIndex = {};for (let i = newStart; i <= newEnd; i++) {keyIndex[newChildren[i].key] = i;}// 记录更新过的节点数量let patched = 0;for (let i = oldStart; i <= oldEnd; i++) {const oldVNode = oldChildren[i];// 如果更新过的节点数量小于需要更新的节点数量,则更新if (patched <= count) {const k = keyIndex[oldVNode.key];if (typeof k !== "undefined") {newVNode = newChildren[k];patch(oldVNode, newVNode, container);source[k - newStart] = i;patched++;if (k < pos) {moved = true;} else {pos = k;}} else {unmount(oldVNode);}} else {// 如果更新过的节点数量大于需要更新的节点数量,则卸载多余节点unmount(oldVNode);}}}
};
11.3 如何移动元素
接下来,我们讨论如何进行DOM移动操作。我们有source索引数组:[2,3,1,-1]

我们要找source的最长递增子序列,它是[2,3],对应的索引是[0,1]。我们重新编号。

在新的一组子节点中,重新编号后索引值为0和1的这两个节点在更新前后顺序没有变化。
即旧节点的p-3和p-4不需要移动。
为了完成节点的移动,我们还需要创建两个索引值i和s:
- i指向新的一组子节点中的最后一个节点
- s指向最长递增子序列中的最后一个元素,即上述的[0,1]的1

如上图所示,我们去掉了旧的一组子节点和无关的线条、变量。
我们开启一个for循环,让i和s按照如图所示的方向移动:
// DOM 移动操作
if (moved) {// 计算最长递增子序列 的 索引const seq = lis(source);// s指向最长递增子序列的最后一个元素let s = seq.length - 1;// i指向新一组节点的最后一个元素let i = count - 1;for (i; i >= 0; i--) {if (i !== seq[s]) {// 说明该节点需要移动} else {s--;}}
}
我们按照这样的思路更新:判断条件 i !== seq[s],如果节点的索引 i 不等于 seq[s]的值,则说明该节点对应的真实 DOM 需要移动。
如图所示,还要考虑source[i]=-1的情况,说明这是一个全新节点,需要挂载:

if (source[i] === -1) {// 说明是新节点,要挂载const pos = i + newStart; // 在新children中的真实位置索引const newVNode = newChildren[pos];// 该节点的下一个节点的位置索引const nextPos = pos + 1;const anchor =nextPos < newChildren.length? newChildren[nextPos].el: null;patch(null, newVNode, container, anchor);
}
挂载完后,i--,往上走:

进行上述2个判断:
- source[i]是否为-1?不是的,说明不是一个新节点
i!==seq[s]?成立。说明此节点需要移动。下面不需要判断了
移动节点的实现思路类似于挂载全新节点,不同点在于,移动节点是通过insert函数完成的:
else if (i !== seq[s]) {// 说明该节点需要移动// 该节点在新节点中的真是位置索引const pos = i + newStart;const newVNode = newChildren[pos];const nextPos = pos + 1;const anchor =nextPos < newChildren.length? newChildren[nextPos].el: null;// 移动insert(newVNode.el, container, anchor);
}
实现完后,状态如下:

进行2个判断:
- source[i]是否为-1?不是的,说明不是一个新节点
i!==seq[s]?不是的,说明它不需要移动- 不需要移动,
s--即可
状态如下:

进行三个判断,得到,s–
然后就结束了。
关于获取最长递增子序列,可以自行搜索。
move相关代码:
// DOM 移动操作
if (moved) {// 计算最长递增子序列 的 索引const seq = lis(source);// s指向最长递增子序列的最后一个元素let s = seq.length - 1;// i指向新一组节点的最后一个元素let i = count - 1;for (i; i >= 0; i--) {if (source[i] === -1) {// 说明是新节点,要挂载const pos = i + newStart; // 在新children中的真实位置索引const newVNode = newChildren[pos];// 该节点的下一个节点的位置索引const nextPos = pos + 1;const anchor =nextPos < newChildren.length? newChildren[nextPos].el: null;patch(null, newVNode, container, anchor);} else if (i !== seq[s]) {// 说明该节点需要移动// 该节点在新节点中的真是位置索引const pos = i + newStart;const newVNode = newChildren[pos];const nextPos = pos + 1;const anchor =nextPos < newChildren.length? newChildren[nextPos].el: null;// 移动insert(newVNode.el, container, anchor);} else {s--;}}
}
完整代码:
const patchKeyedChildren = (n1, n2, container) => {const newChildren = n2.children;const oldChildren = n1.children;// 处理相同前置节点,j指向新旧两组子节点的开头let j = 0;let oldVNode = oldChildren[j];let newVNode = newChildren[j];// 直到遇到不同key值节点为止while (oldVNode.key === newVNode.key) {patch(oldVNode, newVNode, container);j++;oldVNode = oldChildren[j];newVNode = newChildren[j];}// 处理相同后置节点let oldEnd = oldChildren.length - 1;let newEnd = newChildren.length - 1;oldVNode = oldChildren[oldEnd];newVNode = newChildren[newEnd];while (oldVNode.key === newVNode.key) {patch(oldVNode, newVNode, container);oldEnd--;newEnd--;oldVNode = oldChildren[oldEnd];newVNode = newChildren[newEnd];}// 预处理完毕// 挂载新节点if (j > oldEnd && j <= newEnd) {const anchorIndex = newEnd + 1;const anchor =anchorIndex < newChildren.length? newChildren[anchorIndex].el: null;while (j <= newEnd) {patch(null, newChildren[j++], container, anchor);}} else if (j > newEnd && j <= oldEnd) {// 卸载旧节点while (j <= oldEnd) {unmount(oldChildren[j++]);}} else {// 构造source数组// 新的一组子节点中剩余未处理节点的数量const count = newEnd - j + 1;const source = new Array(count);source.fill(-1);const oldStart = j;const newStart = j;let moved = false;let pos = 0;const keyIndex = {};for (let i = newStart; i <= newEnd; i++) {keyIndex[newChildren[i].key] = i;}// 记录更新过的节点数量let patched = 0;for (let i = oldStart; i <= oldEnd; i++) {const oldVNode = oldChildren[i];// 如果更新过的节点数量小于需要更新的节点数量,则更新if (patched <= count) {const k = keyIndex[oldVNode.key];if (typeof k !== "undefined") {newVNode = newChildren[k];patch(oldVNode, newVNode, container);source[k - newStart] = i;patched++;if (k < pos) {moved = true;} else {pos = k;}} else {unmount(oldVNode);}} else {// 如果更新过的节点数量大于需要更新的节点数量,则卸载多余节点unmount(oldVNode);}}// DOM 移动操作if (moved) {// 计算最长递增子序列 的 索引const seq = lis(source);// s指向最长递增子序列的最后一个元素let s = seq.length - 1;// i指向新一组节点的最后一个元素let i = count - 1;for (i; i >= 0; i--) {if (source[i] === -1) {// 说明是新节点,要挂载const pos = i + newStart; // 在新children中的真实位置索引const newVNode = newChildren[pos];// 该节点的下一个节点的位置索引const nextPos = pos + 1;const anchor =nextPos < newChildren.length? newChildren[nextPos].el: null;patch(null, newVNode, container, anchor);} else if (i !== seq[s]) {// 说明该节点需要移动// 该节点在新节点中的真是位置索引const pos = i + newStart;const newVNode = newChildren[pos];const nextPos = pos + 1;const anchor =nextPos < newChildren.length? newChildren[nextPos].el: null;// 移动insert(newVNode.el, container, anchor);} else {s--;}}}}
};
11.4 总结
快速 Diff 算法在实测中性能最优。它借鉴了文本 Diff 中的预处理思路,先处理新旧两组子节点中相同的前置节点和相同的后置节点。当前置节点和后置节点全部处理完毕后,如果无法简单地通过挂载新节点或者卸载已经不存在的节点来完成更新,则需要根据节点的索引关系,构造出一个最长递增子序列。最长递增子序列所指向的节点即为不需要移动的节点。
相关文章:
【Vue.js设计与实现】第三篇第11章:渲染器-快速 Diff 算法-阅读笔记
文章目录 11.1 相同的前置元素和后置元素11.2 判断是否需要进行 DOM 移动操作11.3 如何移动元素11.4 总结 系列目录:【Vue.js设计与实现】阅读笔记目录 非常快的Diff算法。 11.1 相同的前置元素和后置元素 不同于简单 Diff 算法和双端 Diff 算法,…...
材质变体 PSO学习笔记
学习笔记 参考各路知乎大佬文章 首先是对变体的基本认知 概括就是变体是指根据引擎中上层编写(UnityShaderLab/UE连连看)中的各种defines情况,根据不同平台编译成的底层shader,OpenGL-glsl/DX(9-11)-dxbc DX12-dxil/Vulkan-spirv,是打到游…...
2024年【烟花爆竹储存】考试及烟花爆竹储存复审模拟考试
题库来源:安全生产模拟考试一点通公众号小程序 烟花爆竹储存考试参考答案及烟花爆竹储存考试试题解析是安全生产模拟考试一点通题库老师及烟花爆竹储存操作证已考过的学员汇总,相对有效帮助烟花爆竹储存复审模拟考试学员顺利通过考试。 1、【单选题】( …...
文件夹操作
文件夹操作 opendir closedir readdir write(fd,buf,strlen(buf)); return 0; } 作用 : 打开目录 opendir 所有头文件 : #include <sys/types.h> #include <dirent.h> 函数 : DIR *opendir(const char *name); 参数: name :目…...
如何制作一台自己想要的无人机?无人机改装调试技术详解
制作一台符合个人需求的无人机并对其进行改装调试,是一个既具挑战性又充满乐趣的过程。以下是从设计、选购材料、组装、调试到改装的详细步骤: 一、明确需求与设计 1. 明确用途与性能要求: 确定无人机的使用目的,如航拍、比赛、…...
Linux -- 进程间通信、初识匿名管道
目录 进程间通信 什么是进程间通信 进程间通信的一般规律 前言: 管道 代码预准备: 如何创建管道 -- pipe 函数 参数: 返回值: wait 函数 参数: 验证管道的运行: 源文件 test.c : m…...
网站的SSL证书快到期了怎么办?怎么续签?
网站的SSL证书即将到期时,需要续签一个新的证书以保持网站的安全性和信任度。以下是续签SSL证书的一般步骤: 1. 选择证书提供商 如果您之前使用的是免费证书,您可以选择继续使用同一提供商的免费证书服务进行续签。如果您需要更高级别的证书…...
解決爬蟲代理連接的方法
爬蟲在運行過程中常常會遇到代理連接的問題,這可能導致數據抓取的效率降低甚至失敗。 常見的代理連接問題 代理IP失效:這是最常見的問題之一。有些代理IP可能在使用一段時間後失效,導致連接失敗。 連接超時:由於網路不穩定或代…...
Prometheus 监控Harbor
你好!今天分享的是基于Prometheus监控harbor服务。 在之前的文章中分别介绍了harbor基于离线安装的高可用汲取设计和部署。那么,如果我们的harbor服务主机或者harbor服务及组件出现异常,我们该如何快速处理呢? Harbor v2.2及以上…...
SQL 干货 | SQL 半连接
大多数数据库开发人员和管理员都熟悉标准的内、外、左和右连接类型。虽然可以使用 ANSI SQL 编写这些连接类型,但还有一些连接类型是基于关系代数运算符的,在 SQL 中没有语法表示。今天我们将学习一种这样的连接类型:半连接(Semi …...
洛谷 P1226:【模板】快速幂
【题目来源】https://www.luogu.com.cn/problem/P1226【题目描述】 给你三个整数 a,b,p,求 a^b mod p。【输入格式】 输入只有一行三个整数,分别代表 a,b,p。【输出格式】 输出一行一个字符串 a^b mod ps&a…...
nginx常规操作
Linux下查找Nginx配置文件位置 1、查看Nginx进程 ps -aux | grep nginx 圈出的就是Nginx的二进制文件 2、测试Nginx配置文件 /usr/sbin/nginx -t 可以看到nginx配置文件位置 3、nginx的使用(启动、重启、关闭) 首先利用配置文件启动nginx。 nginx -c /usr/local/nginx/conf…...
Docker镜像不能访问
Get "https://registry-1.docker.io/v2/": dial tcp 192.168.10.194:443: connect: connection refused Idea推送镜像至Harbor私服,报以上错误,Docker镜像地址不能访问,更新Harbor服务器Docker镜像地址,重启Docker服务…...
TCP simultaneous open测试
源代码 /*************************************************************************> File Name: common.h> Author: hsz> Brief:> Created Time: 2024年10月23日 星期三 09时47分51秒**********************************************************************…...
Spring 配置文件动态读取pom.xml中的属性
需求: 配置文件中的 spring.profiles.active${env}需要打包时动态绑定。 一、方案: 在pom.xml文件中配置启用占位符替换 <profiles><!-- 本地开发 --><profile><id>dev</id><properties><env>dev</env>…...
Konva 组,层级
代码: <template><div class"rect"><div class"header"> <!-- <el-button type"primary" click"show">展示</el-button>--> <!-- <el-button type"success&quo…...
vue图片加载失败的图片
1.vue图片加载失败的图片 这个问题发生在测试环境和开发本地,线上环境是可以的,测试环境估计被第三方屏蔽了 2.图片有,却加载不出来 <template v-slot:imageUrlsSlots"{ row }"><div class"flexRow rowCenter"&…...
终止,半成收入来自海外,收入可持续性被质疑
芬尼科技终止原因如下:芬尼科技4年期间经历了两次IPO失败,公司半成收入来自海外,然而公司泳池收入面临欧洲地区冲突冲击及德国新节能措施影响。交易所质疑其收入是否具有可持续性。 作者:Eric 来源:IPO魔女 9月25日&a…...
日常记录,使用springboot,vue2,easyexcel使实现字段的匹配导入
目前的需求是数据库字段固定,而excel的字段不固定,需要实现excel导入到一个数据库内。 首先是前端的字段匹配,显示数据库字段和表头字段 读取表头字段: 我这里实现的是监听器导入,需要新建一个listen类。 读Excel …...
Unable to open nested entry ‘********.jar‘ 问题解决
今天把现网版本的task的jar拖回来然后用7-zip打开拖了一个jar进去替换mysql-connector-java-5.1.47.jar 为 mysql-connector-java-5.1.27.jar 启动微服务的时候就报错下面的 Exception in thread "main" java.lang.IllegalStateException: Failed to get nested ar…...
2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...
Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件
今天呢,博主的学习进度也是步入了Java Mybatis 框架,目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学,希望能对大家有所帮助,也特别欢迎大家指点不足之处,小生很乐意接受正确的建议&…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
Xen Server服务器释放磁盘空间
disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...
NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合
在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...
探索Selenium:自动化测试的神奇钥匙
目录 一、Selenium 是什么1.1 定义与概念1.2 发展历程1.3 功能概述 二、Selenium 工作原理剖析2.1 架构组成2.2 工作流程2.3 通信机制 三、Selenium 的优势3.1 跨浏览器与平台支持3.2 丰富的语言支持3.3 强大的社区支持 四、Selenium 的应用场景4.1 Web 应用自动化测试4.2 数据…...
【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)
前言: 双亲委派机制对于面试这块来说非常重要,在实际开发中也是经常遇见需要打破双亲委派的需求,今天我们一起来探索一下什么是双亲委派机制,在此之前我们先介绍一下类的加载器。 目录 编辑 前言: 类加载器 1. …...
消息队列系统设计与实践全解析
文章目录 🚀 消息队列系统设计与实践全解析🔍 一、消息队列选型1.1 业务场景匹配矩阵1.2 吞吐量/延迟/可靠性权衡💡 权衡决策框架 1.3 运维复杂度评估🔧 运维成本降低策略 🏗️ 二、典型架构设计2.1 分布式事务最终一致…...
Python 高级应用10:在python 大型项目中 FastAPI 和 Django 的相互配合
无论是python,或者java 的大型项目中,都会涉及到 自身平台微服务之间的相互调用,以及和第三发平台的 接口对接,那在python 中是怎么实现的呢? 在 Python Web 开发中,FastAPI 和 Django 是两个重要但定位不…...
2.2.2 ASPICE的需求分析
ASPICE的需求分析是汽车软件开发过程中至关重要的一环,它涉及到对需求进行详细分析、验证和确认,以确保软件产品能够满足客户和用户的需求。在ASPICE中,需求分析的关键步骤包括: 需求细化:将从需求收集阶段获得的高层需…...
