详解Vue2的diff算法


Posted in Vue.js onJanuary 06, 2021

前言

双端比较算法是vue2.x采用的diff算法,本篇文章只是对双端比较算法粗略的过程进行了一下分析,具体细节还是得Vue源码,Vue的源码在这

过程

假设当前有两个数组arr1和arr2

let arr1 = [1,2,3,4,5]
let arr2 = [4,3,5,1,2]

那么其过程有五步

  1.  arr1[0] 和 arr2[0]比较
  2.  arr1[ arr1.length-1 ] 和 arr2[ arr2.length-1 ] 比较
  3.  arr1[0] 和 arr2[ arr2.length-1 ] 比较
  4.  arr1[ arr1.length-1 ] 和 arr2[0] 比较
  5.  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图来表示

详解Vue2的diff算法

上代码

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.新增元素

详解Vue2的diff算法

2.减少元素

详解Vue2的diff算法

以上就是详解Vue2的diff算法的详细内容,更多关于Vue2的diff算法的资料请关注三水点靠木其它相关文章!

Vue.js 相关文章推荐
vue中音频wavesurfer.js的使用方法
Feb 20 Vue.js
vue+element_ui上传文件,并传递额外参数操作
Dec 05 Vue.js
vue 基于abstract 路由模式 实现页面内嵌的示例代码
Dec 14 Vue.js
vue-cli4.0多环境配置变量与模式详解
Dec 30 Vue.js
vue3.0中友好使用antdv示例详解
Jan 05 Vue.js
如何在Vue项目中添加接口监听遮罩
Jan 25 Vue.js
Vue 实现可视化拖拽页面编辑器
Feb 01 Vue.js
vue脚手架项目创建步骤详解
Mar 02 Vue.js
vue前端工程的搭建
Mar 31 Vue.js
vue+spring boot实现校验码功能
May 27 Vue.js
vue使用localStorage持久性存储实现评论列表
Apr 14 Vue.js
vue实现登陆页面开发实践
May 30 Vue.js
vuex的使用步骤
Jan 06 #Vue.js
vue3.0中友好使用antdv示例详解
Jan 05 #Vue.js
基于Vue2实现移动端图片上传、压缩、拖拽排序、拖拽删除功能
Jan 05 #Vue.js
Vue+scss白天和夜间模式切换功能的实现方法
Jan 05 #Vue.js
jenkins自动构建发布vue项目的方法步骤
Jan 04 #Vue.js
vue3弹出层V3Popup实例详解
Jan 04 #Vue.js
vue3自定义dialog、modal组件的方法
Jan 04 #Vue.js
You might like
雄兵连三大错觉:凯莎没了,凉冰阵亡了,华烨觉得自己又行了
2020/04/09 国漫
德生PL990,目前市面上唯一一款便携式插卡蓝牙全波段高性能收音机
2021/03/02 无线电
使用 eAccelerator加速PHP代码的目的
2007/03/16 PHP
PHP 抽象方法与抽象类abstract关键字介绍及应用
2014/10/16 PHP
微信公众平台开发教程⑤ 微信扫码支付模式介绍
2019/04/10 PHP
Laravel 创建指定表 migrate的例子
2019/10/09 PHP
PHP基于array_unique实现二维数组去重
2020/07/14 PHP
Javascript 同时提交多个Web表单的方法
2009/02/19 Javascript
Javascript面向对象设计一 工厂模式
2011/12/20 Javascript
firefox下jquery iframe刷新页面提示会导致重复之前动作
2012/12/17 Javascript
关于JS中的apply,call,bind的深入解析
2016/04/05 Javascript
基于RequireJS和JQuery的模块化编程——常见问题全面解析
2016/04/14 Javascript
详细探究ES6之Proxy代理
2016/07/22 Javascript
jQuery stop()用法实例详解
2016/07/28 Javascript
js实现砖头在页面拖拉效果
2020/11/20 Javascript
JavaScript队列、优先队列与循环队列
2016/11/14 Javascript
轻松理解JavaScript之AJAX
2017/03/15 Javascript
js实现放大镜特效
2017/05/18 Javascript
Vue内容分发slot(全面解析)
2017/08/19 Javascript
解析原来浏览器原生支持JS Base64编码解码
2019/08/12 Javascript
Python标准库06之子进程 (subprocess包) 详解
2016/12/07 Python
解决nohup重定向python输出到文件不成功的问题
2018/05/11 Python
python读取csv和txt数据转换成向量的实例
2019/02/12 Python
Python实现的IP端口扫描工具类示例
2019/02/15 Python
基于MATLAB和Python实现MFCC特征参数提取
2019/08/13 Python
Python元组 tuple的概念与基本操作详解【定义、创建、访问、计数、推导式等】
2019/10/30 Python
使用Python 自动生成 Word 文档的教程
2020/02/13 Python
Anaconda配置pytorch-gpu虚拟环境的图文教程
2020/04/16 Python
CSS3实现翘边的阴影效果的代码示例
2016/06/13 HTML / CSS
HTML5是什么 HTML5是什么意思 HTML5简介
2012/10/26 HTML / CSS
英国灯具和灯泡网上商店:Lights.co.uk
2018/02/02 全球购物
说一下mysql, oracle等常见数据库的分页实现方案
2012/09/29 面试题
公司行政经理岗位职责
2013/12/24 职场文书
营销部内勤岗位职责
2014/04/30 职场文书
感恩教育月活动总结
2014/07/07 职场文书
MySQL 数据 data 基本操作
2022/05/04 MySQL