我们都知道,在框架中,当dom节点发生变化时,并不会去改变所有的dom结构,而是对应的改变其中需要改变的部分。那我们思考一下,这里面的原理是什么呢?
在看文章之前,我们先来了解一下虚拟dom,然后慢慢分析,找到其中的奥妙!
Virtual DOM
其实就是一棵以 JavaScript 对象(VNode 节点)作为基础的树,用对象属性来描述节点,相当于在js和真实dom中间加来一个缓存。
我们需要对比虚拟dom和真实dom之间的差异,来进行响应的更新。
实现步骤可如下(以Vue为例):
新构建出来的树
和旧树
进行对比(只进行同层对比
),记录两棵树之间的差异
。这个时候我们应该就有疑问了,上面说要进行同层对比,应该怎么去进行对比呢?难道是暴力法一个个比吗?(开发者肯定不会这么傻)
没错!方法就是我们今天要谈的diff算法,下面开始进入正题!
我们在探索之前,首先来看一看diff算法的基本思路:
diff的过程就是调用名为patch
的函数,比较新旧节点,一边比较一边给真实的DOM打补丁
patch
: 可以简单的理解为给当前DOM节点进行更新,并且调用diff算法对比自身的子节点;
key
:Vnode的唯一标识符
(确保节点唯一性),来辨别节点是否相同。
思路
:递增法,通过判断新节点的位置是否是递增的。由此来判断节点是否需要改变。
我们来观察这个图,很明显prevList
的结点索引为0-1-2-3,而nextList
的节点索引也是0-1-2-3,那么它就不需要进行移动。(每一项都比前一项大!)
移动节点所指的节点是DOM节点
。vnode.el指向该节点对应的真实DOM节点。patch方法会将更新过后的DOM节点,赋值给新的vnode的el属性。
这意味着diff在进行时,a、b、d
三个节点是不需要移动的(因为递增),此时的节点标志为2
,我们只需要将c
移动到d
的后面就可以了。
我们之前说了怎么去移动节点,那么如果我们需要去增加节点呢?这样的话旧节点里面就找不到对应的节点了吧。遇到这种情况,我们就应该生成一个新节点
,然后插入到dom
树中。
两个问题:
首先设置一个判断变量flag
,我们将遍历旧节点去和分别和新节点比较,如果发现key值相同的就修改为true
,每次遍历后,如果flag为false
,那么就是新增节点。
找到后我们怎么去插入dom树呢?和前面的移动节点一样,我们直接将c节点
插入到b节点
的后面。(因为是分别循环对比的,所以我们可以判断出它应该被添加在哪个节点后面)
和增加节点思路一样,只不过一个是判断存在,一个是判断不存在。
大家可以看一看这样一张图,我们发现是不是只要把d节点
移动到a节点
的前面就行了。
但是!!!!react中的diff算法并不会这么去做
由于react中的diff算法是由左向右
遍历的,所以它只会一个个的将节点移动到d
的后面,原本只需要将d节点移动到a节点前一步就能完成的操作,它需要移动a,b,c三次。
Vue2.x
中优化了这个问题,下面让我们一起来探索Vue中的diff算法-双端比较
思路
:双端比较,在新旧节点头尾分别放置指针,然后不断的对比合拢。
双端比较步骤(按顺序):
oldStartNode
和newStartNode
对比,若相同两个指针都往右移一位
,不同则进入下一步oldEndNode
和newEndNode
对比,若相同两个指针都往左移一位
,不同则进入下一步oldStartNode
和newEndNode
对比,若相同则将oldStartNode
移动至结尾,oldStartNode
指针右移一位
,newEndNode
指针左移一位
,不同则进入下一步oldEndNode
和newStartNode
对比,若相同则将oldEndNode
移动至开头,oldEndNode
指针左移一位
,newStartNode
指针右移一位
,不同则结束注意
:以上任意步骤成功指针移动后,将重新开始从第一步
开始比较!
d节点
移动到a节点前
,指针移动,重复步骤上图为第一次双端比较指针移动后的结果,我们开始第二次双端比较:
细心的小伙伴可以发现,我们同样的新旧节点对比,Vue2的diff只需要移动一次
节点,但React的diff需要移动三次
节点。可以看到双端比较的优点还是非常明显的!
双端比较确实牛逼,但是并不是每一次都能那么顺利的,如果我四个步骤全部失败,我们该怎么去移动节点呢?
那么我们该怎么办呢?我们拿新列表的第一个节点b
去旧列表进行进行遍历比较,这里会有两种情况,找到相同节点
和没找到相同节点
以上图为例,我们先说找到的情况,在旧节点中找到相同节点b
,将节点b移动到首位
,就像上面的步骤四一样。
如果我们没有在旧列表里面找到节点呢?就像下面这样
我们直接在头部添加e节点
,然后将newStartNode
指针后移
然后开始进行双端对比!重复之前的步骤
我们先来看下图
可以发现,我们的双端比较一直是在第二步中成功,指针将一直移动,直到下图这样
可以看到我们的oldEndNode
已经移动到了b节点
的前面。
此时oldEndNode
已经小于了oldStartNode
,但是新列表中还有剩余的节点,我们只需要将剩余的节点依次插入到oldStartNode
的DOM之前就可以了。为什么是插入oldStartNode
之前呢?原因是剩余的节点在新列表的位置是位于oldStartNode
之前的,如果剩余节点是在oldStartNode
之后,oldStartNode
就会先行对比,其实还是与第四步的思路一样。
聪明的同学就可以发现了,这里的情况和增加节点完全是反过来的。
出于流程,我们还是分析一下吧,还是双端比较,指针不停的移动直到下面这样:
当新列表的newEndIndex
小于newStartIndex
时,我们将旧列表剩余的节点删除即可。
文章只是大致解释了两种diff算法的思路,详细的步骤还需要大家一起去探索和研究。