vue+diff+最全讲解

前言

我们都知道,浏览器的DOM操作是十分昂贵、十分浪费性能的!

Vue通过虚拟DOM的方式优化了这部分性能浪费,它的核心原理是通过diff算法对比新老节点之间的差异,判断哪些节点可以复用,减少DOM操作的浪费,提升性能!

所以,diff算法的本质就是–找出两个vnode之间的差异,尽可能复用节点!

为何采用虚拟DOM

vue的编写者其实给出了自己的答案:

1、为函数式的UI编程方式打开了大门;

2、可以渲染到DOM以外的backend。

针对这两点谈谈自己的理解:

1、为函数式的UI编程方式打开了大门;

每次生成新的ui就需要重新刷新页面代价太过昂贵,虚拟DOM以及diff算法的引入可以最大限度的复用旧的DOM,使得渲染性能大幅提升。

2、可以渲染到DOM以外的backend。

有了虚拟DOM就可以轻松实现跨平台,多平台的core都相同,只是在render到具体平台的时候采取不同的render就好了

前面介绍了diff算法的本质就是比较vnode的异同,那么vnode都包含哪些属性呢?

其实仔细思考下,一个dom节点主要包含三个部分:

<div id='node'>
   <p id='diff'>哈哈</p>
 </div>
复制代码

1、自身的标签名(div)

2、自身的属性(id=’node’)

3、子节点(p)

所以我们可以设计如下的对象结构表示一个dom节点

const oldVnode = {
  tag:'div',
  attrs:{id:'node'},
  children:[{ tag:'p',attrs:{id:'diff'},children:['哈哈']}]
}
复制代码

当用户对界面进行操作,比如把div的id改为dom ,将子节点span的文本子节点‘哈哈’改为‘嘻嘻’,那么我们可以得到如下vnode

const newVnode = {
  tag:'div',
  attrs:{id:'dom'},
  children:[{ tag:'p',attrs:{id:'diff'},children:['嘻嘻']}]
}
复制代码

那么我们运行diff(oldVnode,newVnode),就能知道oldVnode和newVnode之间的差异如下:
div的id改为dom
span的文本子‘哈哈’改为‘嘻嘻
知道了差异部分,我们就能更新视图,伪代码如下:

document.getElementById("app").setAttribute('id', 'app2')// id 改为 app2
document.getElementById("child").firstChild.textContent ='2' //1 改为 2
复制代码

上面整个过程就是整个diff过程的预演,真实的diff过程跟上面整个过程大同小异,只是需要考虑更多的边界条件!下面让我们来了解一下真正的diff算法

diff算法执行的时机

在了解diff算法的执行时机之前,我们先简单看一下vue框架的渲染过程,如下图所示:

WeChat26ad8c12d4b1a73ecbc6637186581444.png

从图中可以看出,当我们更新data中响应式数据的值时,vue的patch()函数执行过程中会通过diff算法对比新老vnode的异同,然后更新vnode树,当对比完成以后会统一将有变更的虚拟接点渲染成前端页面。

diff算法

diff算法包括二部分:

1、vnode树的遍历

2、节点对比,只对比同层的节点

image.png

vnode树的遍历:

虚拟DOM说到底只是一颗树形结构,对于树的遍历我们知道有深度遍历和广度遍历

目前,不管是vue还是react,采用的都是深度遍历算法

深度遍历需要栈结构,可以通过递归(内核维护调用栈)的方式实现,也可以采用人为构造栈,然后循环栈完成深度遍历。通常深度优先搜索法不全部保留结点,扩展完的结点从栈中弹出删去,这样,在栈中存储的结点数就是深度值,因此它占用空间较少。

节点对比:

对于相同的节点,继续比较子节点:

同一级子元素新老虚拟DOM列表分别设置startIndex和endIndex,首先,旧首和新首对比,旧尾和新尾对比,然后交叉判断startIndex和endIndex是否是相同元素

对比的结果有三种情况:相同、新增、删除、移位

相同

保持不变

新增

老的startIndex不动,新的startIndex移位,并在老的startIndex元素前插入

移位:

新startIndex和老startIndex或者新endIndex和老endIndex相同,只要移动startIndex或者endIndex就可以了;

新startIndex和老endIndex相同,新startIndex++,老endIndex–,将老endIndex的ele插入到老startIndex的ele前面

新endIndex和老startIndex相同,新endIndex–,老startIndex++,将老的startIndex的ele插入到老endIndex的ele的后面

新startIndex的key匹配到老的vnode的key,将老vnode的ele插入到老startIndex的ele前面,还有一个操作:将老vnode标记位undefined,(oldCh[idxInOld] = undefined)

删除

等新startIndex和新endIndex合拢,老startIndex和老endIndex之间的非undefined的vnode的ele全部删除,undefined的node代表已经处理过了(移位)

举例说明

1、oldVnode与newVnode头指针指向的节点相同,DOM节点保持不变,oldVnode与newVnode头指针分别向后前一位。

image.png

2、oldVnode与newVnode尾指针指向的节点相同,DOM节点保持不变,oldVnode与newVnode尾指针分别向后退一位。

image.png

3、newVnode尾指针节点与oldVnode头指针节点是同一个节点,将oldVnode头指针对应的DOM插入到oldVnode尾指针对应的DOM之后,newVnode尾指针向后退一位,oldVnode头指针向前移一位。

image.png

(4)oldVnode包含newVnode头指针节点,将newVnode尾指针对应的DOM插入到oldVnode头指针对应的DOM之前,oldCh头指针向前移一位,newCh尾指针向后退一位。

image.png
5、newVnode不包含oldVnode头指针节点,将oldVnode头指针对应的DOM删除,newCh头指针向前移一位,oldCh尾指针向后退一位。

image.png

6、oldVnode不包含newVnode头指针节点,将newVnode头指针节点插入到oldVnode头指针对应的DOM之前,newCh头指针向前移一位。

image.png

7、newVnode已经没了,oldVnode剩余的节点4、6、7说明都被删除了,删除对应dom即可。

image.png

8、最终,DOM完全更新为与newNode一样的结构,diff过程完毕!

参考文献

1、www.jianshu.com/p/081103a62…

2、juejin.cn/post/684490…

3、www.jianshu.com/p/211c7f216…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享