详解Vue2的diff算法

前言

双端比较算法是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图来表示

上代码

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.减少元素

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

(0)

相关推荐

  • 详解vue的diff算法原理

    我的目标是写一个非常详细的关于diff的干货,所以本文有点长.也会用到大量的图片以及代码举例,目的让看这篇文章的朋友一定弄明白diff的边边角角. 先来了解几个点... 1. 当数据发生变化时,vue是怎么更新节点的? 要知道渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?diff算法能够帮助我们. 我们先根据真实DOM生成一颗 virtual DOM ,当

  • vue diff算法全解析

    前言 我们知道 Vue 使用的是虚拟 DOM 去减少对真实 DOM 的操作次数,来提升页面运行的效率.今天我们来看看当页面的数据改变的时候,Vue 是如何来更新 DOM 的.Vue和React在更新dom时,使用的算法基本相同,都是基于 snabbdom. 当页面上的数据发生变化时,Vue 不会立即渲染.而是经过 diff 算法,判断出哪些是不需要变化的,哪些是需要变化更新的,只需要更新那些需要更新的 DOM 就可以了,这样就减少了很多不必要的 DOM 操作,大大提升了性能. Vue就使用了这样

  • Vue实现开心消消乐游戏算法

    之前做过一个算法题,算法要求就是写一个开心消消乐的逻辑算法,当时也是考虑了一段时间才做出来.后来想了想,既然核心算法都有了,能不能实现一个开心消消乐的小游戏呢,于是花了两天时间做了一个小游戏出来. 效果展示# 先在这里放一个最终实现的效果,还是一个比较初级的版本,大家有什么想法欢迎评论哦 游戏规则: 初始时会给玩家十分的初始分,每拖动一次就减一分,每消除一个方块就加一分,直到最后分数为0游戏结束 任意两个方块都可以拖动 界面设计# 页面的布局比较简单,格子的数据是一个二维数组的形式,说到这里大家

  • vue的diff算法知识点总结

    源码:https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js 虚拟dom diff算法首先要明确一个概念就是diff的对象是虚拟dom,更新真实dom则是diff算法的结果 Vnode基类 constructor ( ... ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = u

  • Vue的transition-group与Virtual Dom Diff算法的使用

    开始 这次的题目看上去好像有点奇怪:把两个没有什么关联的名词放在了一起,正如大家所知道的,transition-group就是Vue的内置组件之一主要用在列表的动画上,但是会跟Virtual Dom Diff算法有什么特别的联系吗?答案明显是有的,所以接下来就是代码分解. 缘起 主要是最近对Vue的Virtual Dom Diff算法有点模糊了,然后顺手就打开了电脑准备温故知新:但是很快就留意到代码: // removeOnly is a special flag used only by <t

  • 一篇文章带你搞懂Vue虚拟Dom与diff算法

    前言 使用过Vue和React的小伙伴肯定对虚拟Dom和diff算法很熟悉,它扮演着很重要的角色.由于小编接触Vue比较多,React只是浅学,所以本篇主要针对Vue来展开介绍,带你一步一步搞懂它. 虚拟DOM 什么是虚拟DOM? 虚拟DOM(Virtual   Dom),也就是我们常说的虚拟节点,是用JS对象来模拟真实DOM中的节点,该对象包含了真实DOM的结构及其属性,用于对比虚拟DOM和真实DOM的差异,从而进行局部渲染来达到优化性能的目的. 真实的元素节点: <div id="wr

  • 解析Vue 2.5的Diff算法

    DOM"天生就慢",所以前端各大框架都提供了对DOM操作进行优化的办法,Angular中的是脏值检查,React首先提出了Virtual Dom,Vue2.0也加入了Virtual Dom,与React类似. 本文将对于Vue 2.5.3版本中使用的Virtual Dom进行分析. updataChildren是Diff算法的核心,所以本文对updataChildren进行了图文的分析. 1.VNode对象 一个VNode的实例包含了以下属性,这部分代码在src/core/vdom/v

  • 基于Vue实现电商SKU组合算法问题

    前段时间,公司要做"添加商品"业务模块,这也算是电商业务里面的一个难点了. 令我印象最深的不是什么"组合商品"."关联商品"."关联单品",而是商品SKU的组合问题. 这个问题特别有意思,当时虽然大体上组合成功,总是有些小bug解决不了,然后手上又有别的任务就没仔细研究它. 后来过了一个月,空闲下来专门研究了下,终于把问题解决,有必要记录下这次体验. 先看下在业务中的效果(tips: 如看不清可放大浏览器) 这个相对来说比较麻

  • 详解vue3.0 diff算法的使用(超详细)

    前言:随之vue3.0beta版本的发布,vue3.0正式版本相信不久就会与我们相遇.尤玉溪在直播中也说了vue3.0的新特性typescript强烈支持,proxy响应式原理,重新虚拟dom,优化diff算法性能提升等等.小编在这里仔细研究了vue3.0beta版本diff算法的源码,并希望把其中的细节和奥妙和大家一起分享. 首先我们来思考一些大中厂面试中,很容易问到的问题: 1 什么时候用到diff算法,diff算法作用域在哪里? 2 diff算法是怎么运作的,到底有什么作用? 3 在v-f

  • 详解为什么Vue中不要用index作为key(diff算法)

    前言 Vue 中的 key 是用来做什么的?为什么不推荐使用 index 作为 key?常常听说这样的问题,本篇文章带你从原理来一探究竟. 另外本文的结论对于性能的毁灭是针对列表子元素顺序会交换.或者子元素被删除的特殊情况,提前说明清楚,喷子绕道. 本篇已经收录在 Github 仓库,欢迎 Star: https://github.com/sl1673495/blogs/issues/39 示例 以这样一个列表为例: <ul> <li>1</li> <li>

  • vue2.0中goods选购栏滚动算法的实现代码

    不多说,直接代码,以便以后重复利用: <script type="text/ecmascript-6"> import BScroll from 'better-scroll'; const ERR_OK = 0; export default { props: { sell: { type: Object } }, data() { return { goods: [], listHeight: [], scrollY: 0 }; }, computed: { curre

  • LRU算法在Vue内置组件keep-alive中的使用

    vue的keep-alive内置组件的使用也是使用了改算法,源码如下: export default { name: "keep-alive", // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中 abstract: true, props: { // 被缓存组件 include: patternTypes, // 不被缓存组件 exclude: patternTypes, // 指定缓存大小 max: [String, Numb

随机推荐