详解Vue2的diff算法
作者:啥都不会的前端CV狮 发布时间:2024-04-28 09:30:48
前言
双端比较算法是vue2.x采用的diff算法,本篇文章只是对双端比较算法粗略的过程进行了一下分析,具体细节还是得Vue源码,Vue的源码在这
过程
假设当前有两个数组arr1和arr2
let arr1 = [1,2,3,4,5]
let arr2 = [4,3,5,1,2]
那么其过程有五步
arr1[0] 和 arr2[0]比较
arr1[ arr1.length-1 ] 和 arr2[ arr2.length-1 ] 比较
arr1[0] 和 arr2[ arr2.length-1 ] 比较
arr1[ arr1.length-1 ] 和 arr2[0] 比较
arr2[0] 和 arr1的每个元素进行比较
每次比较都是从数组的两端开始比较,如果是首位比较相等,那么比较的开头索引+1
如果是在末尾比较成功,那么比较的结束索引-1,当开头索引大于结束索引时说明比较已经结束
拆解过程
let arr1 = [1,2,3,4,5]
let arr2 = [4,3,5,1,2]
let oldStartIdx = 0
let oldEndIdx = arr1.lenght -1
let newStartIdx = 0
let newEndIdx = arr2.length -1
let oldStartVNode = arr1[oldStartIdx]
let oldEndVNode = arr1[oldEndIdx]
let newStartVNode = arr2[newStartIdx]
let newEndVNode = arr2[newEndIdx]
第一轮:
1. 1和4比较不相等
2. 5和2比较不相等
3. 1和2比较不相等
4. 5和4比较不相等
5. 4和旧数组逐一比较,和索引为3的值相等,说明4由索引3变换位置为了0, newStartIdx++
//比较完后,使用u_1表示比较成功的元素
[1,2,3,u_1,5] //arr1
[u_1,3,5,1,2] //arr2
第二轮:
1. 1和3比较不相等
2. 5和2比较不相等
3. 1和2比较不相等
4. 5和3比较不相等
5. 3和旧数组逐一比较,和索引为2的值相等,3由索引2变换位置为了0, newStartIdx++
//比较成功后,使用u_2表示比较成功的元素
[1,2,u_2,u_1,5] //arr1
[u_1,u_2,5,1,2] //arr2
第三轮:
1. 1和5比较不相等
2. 5和2比较不相等
3. 1和2比较不相等
4. 5和5比较相等,5已经从旧数组oldEndIdx位置移动到了newStartIdx位置,newStartIdx++, oldEndIdx--
5. 第四步比较成功,进入下一轮
//比较成功后,使用u_3表示比较成功的元素
[1,2,u_2,u_1,u_3] //arr1
[u_1,u_2,u_3,1,2] //arr2
第四轮:
1. 1和1比较相等,1已经从旧数组oldStartIdx位置移动到newStartIdx位置,oldStartIdx++,newStartIdx++
2. 第一步比较成功,进入下一轮 3. 第一步比较成功,进入下一轮
4. 第一步比较成功,进入下一轮 5. 第一步比较成功,进入下一轮
//比较成功后,使用u_4表示比较成功的元素
[u_4,2,u_2,u_1,u_3] //arr1
[u_1,u_2,u_3,u_4,2] //arr2
第五轮:
1. 2和2比较相等,1已经从旧数组oldStartIdx位置移动到newStartIdx位置,oldStartIdx++,newStartIdx++
2. 第一步比较成功,进入下一轮
3. 第一步比较成功,进入下一轮
4. 第一步比较成功,进入下一轮
5. 第一步比较成功,进入下一轮
//比较成功后,使用u_5表示比较成功的元素
[u_4,u_5,u_2,u_1,u_3] //arr1
[u_1,u_2,u_3,u_4,u_5] //arr2
用一个gif图来表示
上代码
function diff(prevChildren, nextChildren) {
let oldStartIdx = 0 //旧数组起始索引
let oldEndIdx = prevChildren.length - 1 //旧数组结束索引
let newStartIdx = 0 //新数组其实索引
let newEndIdx = nextChildren.length - 1 //新数组结束索引
let oldStartVNode = prevChildren[oldStartIdx]
let oldEndVNode = prevChildren[oldEndIdx]
let newStartVNode = nextChildren[newStartIdx]
let newEndVNode = nextChildren[newEndIdx]
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (!oldStartVNode) {
//undefined 时前移一位
oldStartVNode = prevChildren[++oldStartIdx]
} else if (!oldEndVNode) {
//undefined 时后移一位
oldEndVNode = prevChildren[--oldEndIdx]
} else if (oldStartVNode.key === newStartVNode.key ) { //1.开始与开始
oldStartVNode = prevChildren[++oldStartIdx]
newStartVNode = nextChildren[++newStartIdx]
} else if ( oldEndVNode.key === newEndVNode.key ) { //2.结束与结束
oldEndVNode = prevChildren[--oldEndIdx]
newEndVNode = nextChildren[--newEndIdx]
} else if (oldStartVNode.key === newEndVNode.key ) { //3.开始与结束
oldStartVNode = prevChildren[++oldStartIdx]
newEndVNode = nextChildren[--newEndIdx]
} else if (oldEndVNode.key === newStartVNode.key ) { //4.结束与开始
oldEndVNode = prevChildren[--oldEndIdx]
newStartVNode = nextChildren[++newStartIdx]
} else {
//5.新数组开头元素和旧数组每一个元素对比
const idxInOld = prevChildren.findIndex((node) => {
if (node && node.key === newStartVNode.key) {
return true
}
})
if (idxInOld >= 0) {
prevChildren[idxInOld] = undefined
} else {
//newStartVNode是新元素
}
newStartVNode = nextChildren[++newStartIdx]
}
}
}
diff([1,2,3,4,5],[4,3,5,1,2])
我们发现,上面的算法走完后,如果新旧两个数组只是顺序变化,那么它能完美的diff出差异,但是如果新数组有新增或者删除的时候就不行了,因此我们在while循环完成后需要找出新增或者删除的元素,那怎么知道哪些是新增哪些是删除的元素呢?
在比较的第五步,选取的新数组的第一个元素和旧数组的所有元素逐一对比,这里我们就可以得出了这个数组是否是新增,如果对比相等,那就是位置变换,否则当前元素就是新增的,但是,while循环的条件是oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,如果是以下情况
let arr1 = [1,2,3,4,5]
let arr2 = [1,2,3,4,5,6,7]
因为循环条件的导致,这里会在5次while后就结束了,因此在数组末尾的6和7永远走不了第五步的插入条件,那如何判断6和7是新增的呢?我们来观察一下while循环结束后的索引
//例子1
let arr1 = [1,2,3,4,5]
let arr2 = [1,2,3,4,5,6,7]
//diff后它们的索引为
oldStartIdx = 5, oldEndIdx = 4
newStartIdx = 5, newEndIdx = 6
//例子2
let arr1 = [1,2,3,4,5]
let arr2 = [4,5,6,7,1,3,2]
//diff后它们的索引为
oldStartIdx = 3, oldEndIdx = 2
newStartIdx = 6, newEndIdx = 5
//例子3
let arr1 = [1,2,3,4,5]
let arr2 = [7,1,3,5,6,4,2]
//diff后它们的索引为
oldStartIdx = 5, oldEndIdx = 4
newStartIdx = 4, newEndIdx = 4
//例子4
let arr1 = [1,2,3,4,5]
let arr2 = [2,4,1,5,7,3,6]
//diff后它们的索引为
oldStartIdx = 3, oldEndIdx = 2
newStartIdx = 6, newEndIdx = 6
我们发现,新增元素的索引和newStartIdx还有newEndIdx是一一对应的
例子1:newStartIdx小于newEndIdx,并且是5和6,而新增元素6对应在arr2的索引为6,新增元素7对应在arr2的索引为7,此时6和7都已经越界出arr1的长度范围
例子2:newStartIdx是大于newEndIdx,没有对应关系
例子3:newStartIdx等于newEndIdx,我们发现arr2索引为4的元素正是新增元素6,但是6次时没有越界出arr1的长度范围,它刚好在数组的最后一个元素
例子4:newStartIdx等于newEndIdx,arr2中索引为6的值正是新增元素6
那么得出的结论就是,如果在while循环结束后,如果newStartIdx是小于或者等于newEndIdx,那么在newStartIdx和newEndIdx索引之间对应的元素就是新增的元素,并且oldStartIdx总是比oldEndIdx大
上面说完了新增,那如果是删除元素呢?看例子
//例子1
let arr1 = [4,3,5,6,7,2,1]
let arr2 = [1,3,5,4,2]
//diff后它们的索引为
oldStartIdx = 3, oldEndIdx = 4
newStartIdx = 3, newStartIdx = 2
//例子2
let arr1 = [7,2,3,5,6,1,4]
let arr2 = [5,1,2,3,4]
//diff后它们的索引为
oldStartIdx = 0, oldEndIdx = 4
newStartIdx = 4, newStartIdx = 3
//例子3
let arr1 = [1,5,4,2,6,7,3]
let arr2 = [4,5,1,2,3]
//diff后它们的索引为
oldStartIdx = 4, oldEndIdx = 5
newStartIdx = 4, newStartIdx = 3
同理新增的观察套路,发现newStartIdx总是比newStartIdx大,并且需要删除的元素总是在oldStartIdx和oldEndIdx对应的索引之间,那么我们只需要把oldStartIdx和oldEndIdx的元素删除即可,那问题来了,像例子2 中oldStartIdx和oldEndIdx索引之间的元素有7,2,3,5,6其中真正需要删除的只有7和6,这样子不就误删了2,3,5么?关键的来了,我们看例子2的2,3,5发现它们走的都是双端比较算法的第五步,第五步写的代码是
const idxInOld = prevChildren.findIndex((node) => {
if (node && node.key === newStartVNode.key) {
return true
}
})
if (idxInOld >= 0) {
prevChildren[idxInOld] = undefined
} else {
//newStartVNode是新元素
}
newStartVNode = nextChildren[++newStartIdx]
如果idxInOld>0说明在旧数组中找到了,那么我们将preChildren[idxInOld]设置为undefined,也就是说2,3,5经过diff算法后,它们在arr1中的值已经被替换为了undefined,这里也是就为什么在diff算法开始需要判断!oldStartVNode和!oldEndVnode的原因了,下面我们完善代码
function diff(prevChildren, nextChildren) {
let oldStartIdx = 0 //旧数组起始索引
let oldEndIdx = prevChildren.length - 1 //旧数组结束索引
let newStartIdx = 0 //新数组其实索引
let newEndIdx = nextChildren.length - 1 //新数组结束索引
let oldStartVNode = prevChildren[oldStartIdx]
let oldEndVNode = prevChildren[oldEndIdx]
let newStartVNode = nextChildren[newStartIdx]
let newEndVNode = nextChildren[newEndIdx]
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (!oldStartVNode) { //undefined 时前移一位
oldStartVNode = prevChildren[++oldStartIdx]
} else if (!oldEndVNode) {
//undefined 时后移一位
oldEndVNode = prevChildren[--oldEndIdx]
} else if (oldStartVNode.key === newStartVNode.key ) { //1.开始与开始
oldStartVNode = prevChildren[++oldStartIdx]
newStartVNode = nextChildren[++newStartIdx]
} else if ( oldEndVNode.key === newEndVNode.key ) { //2.结束与结束
oldEndVNode = prevChildren[--oldEndIdx]
newEndVNode = nextChildren[--newEndIdx]
} else if (oldStartVNode.key === newEndVNode.key ) { //3.开始与结束
oldStartVNode = prevChildren[++oldStartIdx]
newEndVNode = nextChildren[--newEndIdx]
} else if (oldEndVNode.key === newStartVNode.key ) { //4.结束与开始
oldEndVNode = prevChildren[--oldEndIdx]
newStartVNode = nextChildren[++newStartIdx]
} else {
//5.新数组开头元素和旧数组每一个元素对比
const idxInOld = prevChildren.findIndex((node) => {
if (node && node.key === newStartVNode.key) {
return true
}
})
if (idxInOld >= 0) {
prevChildren[idxInOld] = undefined
} else {
//newStartVNode是新元素
}
newStartVNode = nextChildren[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
//新增内容
let vnode = nextChildren[newStartIdx]
}
} else if (newStartIdx > newEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) { /
/删除内容
}
}
}
diff([1,2,3,4,5],[4,3,5,1,2])
接下来我们使用两个gif图来表示一下diff过程
1.新增元素
2.减少元素
来源:https://juejin.cn/post/6914197387430871047
猜你喜欢
- 开发环境的搭建是一件入门比较头疼的事情,在上期的文稿基础上,增加一项Anaconda的安装介绍。Anaconda是Python的一个发行版本
- 前言pandas 是基于 Numpy 构建的含有更高级数据结构和工具的数据分析包类似于 Numpy 的核心是 ndarray,pandas
- 新建py文件,在里面写入Python代码,代码的功能是打印10次“Hello!”,将代码文件保存到桌面上:在命令行中运行Python脚本,并
- 有时候使用python从网站上爬数据的时候,如果数据里包含中文,有时候显示的却是如下所示...\xe4\xba\xba\xef\xbc\x8
- 用python编表白程序的方法:1、创建GUI窗口,实现代码的调用。2、编写点击触发函数,实现表白程序。具体代码如下:from tkinte
- if语句用来检验一个条件, 如果 条件为真,我们运行一块语句(称为 if-块 ), 否则 我们处理另外一块语句(称为 else-块 )。 e
- 对于时间的选择问题,查到的大部分为两种情况:1.存在readonly属性的2.没有readonly属性的可直接赋值send_keys()测试
- 有时,为了方便看数据的变化情况,需要画一个动态图来看整体的变化情况。主要就是用Matplotlib库。首先,说明plot函数的说明。plt.
- Mysql的Bin log数据恢复:不小心删除数据库前言:因为不小心删除了测试机器上Mysql的一整个数据库Schema,因为是测试机所以没
- 本文实例讲述了Python列表切片操作。分享给大家供大家参考,具体如下:切片指的是列表的一部分。1 基本用法指定第一个元素和最后一个元素的索
- 本文实例讲述了Python实现基于KNN算法的笔迹识别功能。分享给大家供大家参考,具体如下:需要用到:Numpy库Pandas库手写识别数据
- 对所有数据进行整合与管理当你使用SQL Server 2008企业级的数据仓库平台时,你可以高效的操纵所有数据,并对其进行统一管理存储。◆合
- 1.雷达图程序示例'''1.空白极坐标图'''import matplotlib.pyplo
- python命名规则命名风格python几种不同命名风格驼峰式命名法(WjW)混合式命名法(wjWj)大写(WJWJWJ)或大写加下划线(W
- 前言在工作中使用的是oracle数据库,平时想在家测试一些sql是否可以跑的过,可惜自己电脑并没有安装oracle数据库,甚至完全不想安装到
- 一般情况下只有需要长期运行的项目才会去关注内存的增长情况,即使是很小部分的内存泄露经过长期的运行仍然会产生很大的隐患。python本身也是支
- 比较说明1、break和continue是python两个关键字2、break和continue只能用在循环中3、break是终止循环的执行
- script中。let data={....};let url=xx;方法各异:GET:this.$ajax.get(url,{  
- 一、同步原理基于Mysql的binlog日志订阅:binlog日志是Mysql用来记录数据实时的变化Mysql数据同步到ES中分为两种,分别
- 前言在JavaScript中,数据类型分为两大类,一种是基础数据类型,另一种则是复杂数据类型,又叫引用数据类型基础数据类型:数字Number