深刻Vue2.x的虚拟DOM diff原理

1、前言

Vue的核心是双向绑定和虚拟DOM(下文咱们简称为vdom),关于双向绑定能够参阅木琴的文章《剖析Vue原理&实现双向绑定MVVM》,vdom是树状结构,其节点为vnode,vnode和浏览器DOM中的Node一一对应,经过vnode的elm属性能够访问到对应的Node。html

vdom由于是纯粹的JS对象,因此操做它会很高效,可是vdom的变动最终会转换成DOM操做,为了实现高效的DOM操做,一套高效的虚拟DOM diff算法显得颇有必要。前端

Vue的diff算法是基于snabbdom改造过来的,感兴趣的朋友能够选择查阅。vue

640?wx_fmt=png&wxfrom=5&wx_lazy=1

这是一张很经典的图,出自《React’s diff algorithm》,Vue的diff算法也一样,即仅在同级的vnode间作diff,递归地进行同级vnode的diff,最终实现整个DOM树的更新。那同级vnode diff的细节又是怎样的呢?正是本文所要讲的。node

2、例子

咱们在下文中将使用这个简化的例子来说述diff的过程算法

0?wx_fmt=png

如上图的例子,更新前是1到10排列的Node列表,更新后是乱序排列的Node列表。罗列一下图中有如下几种类型的节点变化状况:浏览器

(1)、头部相同、尾部相同的节点:如一、10dom

(2)、头尾相同的节点:如二、9(处理完头部相同、尾部相同节点以后)ide

(3)、新增的节点:11post

(4)、删除的节点:8性能

(5)、其余节点:三、四、五、六、7

3、简单的diff

简单的diff算法能够这样设计:

逐个遍历newVdom的节点,找到它在oldVdom中的位置,若是找到了就移动对应的DOM元素,若是没找到说明是新增节点,则新建一个节点插入。遍历完成以后若是oldVdom中还有没处理过的节点,则说明这些节点在newVdom中被删除了,删除它们便可。

仔细思考一下,几乎每一步都要作移动DOM的操做,这在DOM总体结构变化不大时的开销是很大的,实际上DOM变化不大的状况现实中常常发生,不少时候咱们只须要变动某个节点的文本而已。

接下来咱们看一下Vue的diff实现

4、Vue的diff实现

上图例子中我画上了oldStart+oldEnd,newStart+newEnd这样2对指针,分别对应oldVdom和newVdom的起点和终点。起止点以前的节点是待处理的节点,Vue不断对vnode进行处理同时移动指针直到其中任意一对起点和终点相遇。处理过的节点Vue会在oldVdom和newVdom中同时将它标记为已处理(标记方法后文中有介绍)。Vue经过如下措施来提高diff的性能。

(一)、优先处理特殊场景

(1)、头部的同类型节点、尾部的同类型节点

这类节点更新先后位置没有发生变化,因此不用移动它们对应的DOM

(2)、头尾/尾头的同类型节点

这类节点位置很明确,不须要再花心思查找,直接移动DOM就好

处理了这些场景以后,一方面一些不须要作移动的DOM获得快速处理,另外一方面待处理节点变少,缩小了后续操做的处理范围,性能也获得提高

(二)、“原地复用”

“原地复用”是指Vue会尽量复用DOM,尽量不发生DOM的移动。Vue在判断更新先后指针是否指向同一个节点,其实不要求它们真实引用同一个DOM节点,实际上它仅判断指向的是不是同类节点(好比2个不一样的div,在DOM上它们是不同的,可是它们属于同类节点),若是是同类节点,那么Vue会直接复用DOM,这样的好处是不须要移动DOM。再看上面的实例,假如10个节点都是div,那么整个diff过程当中就没有移动DOM的操做了。

“原地复用”在Vue的官方文档中有提到,虽然带来了好处,可是也会产生一些问题,朋友们能够复习一下

https://cn.vuejs.org/v2/guide/list.html#key

https://cn.vuejs.org/v2/guide/conditional.html#用-key-管理可复用的元素

5、按步解剖实例

(一)、总体视图

0?wx_fmt=png

先看一张总体视图,整个diff分两部分:

(1)、第一部分是一个循环,循环内部是一个分支逻辑,每次循环只会进入其中的一个分支,每次循环会处理一个节点,处理以后将节点标记为已处理(oldVdom和newVdom都要进行标记,若是节点只出如今其中某一个vdom中,则另外一个vdom中不须要进行标记),标记的方法有2种,当节点正好在vdom的指针处,移动指针将它排除到未处理列表以外便可,不然就要采用其余方法,Vue的作法是将节点设置为undefined。

(2)、循环结束以后,可能newVdom或者oldVdom中还有未处理的节点,若是是newVdom中有未处理节点,则这些节点是新增节点,作新增处理。若是是oldVdom中有这类节点,则这些是须要删除的节点,相应在DOM树中删除之

整个过程是逐步找到更新先后vdom的差别,而后将差别反应到DOM树上(也就是patch),特别要提一下Vue的patch是即时的,并非打包全部修改最后一块儿操做DOM(React则是将更新放入队列后集中处理),朋友们会问这样作性能不好吧?实际上现代浏览器对这样的DOM操做作了优化,并没有差异。

(二)、逐步解析

(1)、处理头部的同类型节点,即oldStart和newStart指向同类节点的状况,以下图中的节点1

这种状况下,将节点1的变动更新到DOM,而后对其进行标记,标记方法是oldStart和newStart后移1位便可,过程当中不须要移动DOM(更新DOM或许是要的,好比属性变动了,文本内容变动了等等)

0?wx_fmt=png

(2)、处理尾部的同类型节点,即oldEnd和newEnd指向同类节点的状况,以下图中的节点10

与状况(1)相似,这种状况下,将节点10的变动更新到DOM,而后oldEnd和newEnd前移1位进行标记,一样也不须要移动DOM

0?wx_fmt=png

(3)、处理头尾/尾头的同类型节点,即oldStart和newEnd,以及oldEnd和newStart指向同类节点的状况,以下图中的节点2和节点9

先看节点2,实际上是日后移了,移到哪里?移到oldEnd指向的节点(即节点9)后面,移动以后标记该节点,将oldStart后移1位,newEnd前移一位

0?wx_fmt=png

操做结束以后状况以下图

0?wx_fmt=png

一样地,节点9也是相似的处理,处理完以后成了下面这样

0?wx_fmt=png

(4)、处理新增的节点

newStart来到了节点11的位置,在oldVdom中找不到节点11,说明它是新增的

那么就建立一个新的节点,插入DOM树,插到什么位置?插到oldStart指向的节点(即节点3)前面,而后将newStart后移1位标记为已处理(注意oldVdom中没有节点11,因此标记过程当中它的指针不须要移动),处理以后以下图

0?wx_fmt=png

(5)、处理更新的节点

通过第(4)步以后,newStart来到了节点7的位置,在oldVdom中能找到它并且不在指针位置(查找oldVdom中oldStart到oldEnd区间内的节点),说明它的位置移动了

那么须要在DOM树中移动它,移到哪里?移到oldStart指向的节点(即节点3)前面,与此同时将节点标记为已处理,跟前面几种状况有点不一样,newVdom中该节点在指针下,能够移动newStart进行标记,而在oldVdom中该节点不在指针处,因此采用设置为undefined的方式来标记(必定要标记吗?后面会提到)

0?wx_fmt=png

处理以后就成了下面这样

0?wx_fmt=png

(6)、处理三、四、五、6节点

通过第(5)步处理以后,咱们看到了使人欣慰的一幕,newStart和oldStart又指向了同一个节点(即都指向节点3),很简单,按照(1)中的作法只需移动指针便可,很是高效,三、四、五、6都如此处理,处理完以后以下图

0?wx_fmt=png

(7)、处理需删除的节点

通过前6步处理以后(实际上前6步是循环进行的),朋友们看newStart跨过了newEnd,它们相遇啦!而这个时候,oldStart和oldEnd尚未相遇,说明这2个指针之间的节点(包括它们指向的节点,即上图中的节点七、节点8)是这次更新中被删掉的节点。

OK,那咱们在DOM树中将它们删除,再回到前面咱们对节点7作了标记,为何标记是必需的?标记的目的是告诉Vue它已经处理过了,是须要出如今新DOM中的节点,不要删除它,因此在这里只需删除节点8。

在应用中也可能会遇到oldVdom的起止点相遇了,可是newVdom的起止点没有相遇的状况,这个时候须要对newVdom中的未处理节点进行处理,这类节点属于更新中被加入的节点,须要将他们插入到DOM树中。

0?wx_fmt=png

至此,整个diff过程结束了

Vue的diff算法与动态规划算法中的经典案例“计算a到b的最小编辑距离”看上去有些类似,实际彻底不一样,Vue的diff相对来讲轻量不少,感兴趣的朋友能够查阅相关资料进行了解。

好啦,感谢你的阅读,但愿能帮助你理解Vue的diff算法,在阅读过程当中遇到的问题也欢迎一块儿交流!

火热招聘?:SNG增值产品部企鹅电竞、鹅漫U品、QQ动漫、QQ会员、手Q游戏等核心业务开发岗位火热招聘中,诚招终端、后台、前端岗位人才,欢迎有实力有梦想的你一块儿加入玩转“增值”世界!

相关文章
相关标签/搜索