【Diff算法图解】带你探索React、Vue2.x的Diff算法

文章目录

  • 前言
  • 一、Virtual DOM(虚拟dom)
  • 二、React Diff
    • 实现思想
    • 移动节点
    • 增加节点
    • 移除节点
    • React Diff的缺陷
  • 三、Vue2.X Diff
    • 实现思想
    • 移动节点
    • 特殊情况
    • 增加节点
    • 移除节点
  • 总结


前言

我们都知道,在框架中,当dom节点发生变化时,并不会去改变所有的dom结构,而是对应的改变其中需要改变的部分。那我们思考一下,这里面的原理是什么呢?

在看文章之前,我们先来了解一下虚拟dom,然后慢慢分析,找到其中的奥妙!

一、Virtual DOM(虚拟dom)

Virtual DOM 其实就是一棵以 JavaScript 对象(VNode 节点)作为基础的树,用对象属性来描述节点,相当于在js和真实dom中间加来一个缓存。

我们需要对比虚拟dom和真实dom之间的差异,来进行响应的更新。

实现步骤可如下(以Vue为例):

  1. 用JavaScript对象结构表述dom树的结构,然后用这个树构建一个真正的dom树,插到浏览器的页面中。
  2. 当状态改变了,也就是我们的state做出修改,vue便会重新构造一棵树的对象树,然后用这个新构建出来的树旧树进行对比(只进行同层对比),记录两棵树之间的差异
  3. 把2记录的差异在重新应用到步骤1所构建的真正的dom树,视图就更新了。

这个时候我们应该就有疑问了,上面说要进行同层对比,应该怎么去进行对比呢?难道是暴力法一个个比吗?(开发者肯定不会这么傻)

没错!方法就是我们今天要谈的diff算法,下面开始进入正题!

我们在探索之前,首先来看一看diff算法的基本思路:

  • 只比较同一级,不跨级比较
  • tag不相同,直接删掉重建,不再深度比较
  • tag和key,两者都相同,则认为是相同节点,不在深度比较

diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁

patch: 可以简单的理解为给当前DOM节点进行更新,并且调用diff算法对比自身的子节点;

key:Vnode的唯一标识符(确保节点唯一性),来辨别节点是否相同。

二、React Diff

实现思想

思路:递增法,通过判断新节点的位置是否是递增的。由此来判断节点是否需要改变。
【Diff算法图解】带你探索React、Vue2.x的Diff算法_第1张图片
我们来观察这个图,很明显prevList的结点索引为0-1-2-3,而nextList的节点索引也是0-1-2-3,那么它就不需要进行移动。(每一项都比前一项大!)

移动节点

移动节点所指的节点是DOM节点。vnode.el指向该节点对应的真实DOM节点。patch方法会将更新过后的DOM节点,赋值给新的vnode的el属性。

【Diff算法图解】带你探索React、Vue2.x的Diff算法_第2张图片
我们来观察这张图,我们的新节点位置为0-1-3-2

这意味着diff在进行时,a、b、d三个节点是不需要移动的(因为递增),此时的节点标志为2,我们只需要将c移动到d的后面就可以了。

增加节点

【Diff算法图解】带你探索React、Vue2.x的Diff算法_第3张图片

我们之前说了怎么去移动节点,那么如果我们需要去增加节点呢?这样的话旧节点里面就找不到对应的节点了吧。遇到这种情况,我们就应该生成一个新节点,然后插入到dom树中。

两个问题:

  1. 怎么发现存在新的节点?
  2. 怎么插入dom树

首先设置一个判断变量flag,我们将遍历旧节点去和分别和新节点比较,如果发现key值相同的就修改为true,每次遍历后,如果flag为false,那么就是新增节点。

找到后我们怎么去插入dom树呢?和前面的移动节点一样,我们直接将c节点插入到b节点的后面。(因为是分别循环对比的,所以我们可以判断出它应该被添加在哪个节点后面)

移除节点

和增加节点思路一样,只不过一个是判断存在,一个是判断不存在。

React Diff的缺陷

【Diff算法图解】带你探索React、Vue2.x的Diff算法_第4张图片
大家可以看一看这样一张图,我们发现是不是只要把d节点移动到a节点的前面就行了。

但是!!!!react中的diff算法并不会这么去做
【Diff算法图解】带你探索React、Vue2.x的Diff算法_第5张图片
由于react中的diff算法是由左向右遍历的,所以它只会一个个的将节点移动到d的后面,原本只需要将d节点移动到a节点前一步就能完成的操作,它需要移动a,b,c三次。

Vue2.x中优化了这个问题,下面让我们一起来探索Vue中的diff算法-双端比较

三、Vue2.X Diff

实现思想

思路:双端比较,在新旧节点头尾分别放置指针,然后不断的对比合拢。

双端比较步骤(按顺序):

  1. oldStartNodenewStartNode对比,若相同两个指针都往右移一位,不同则进入下一步
  2. oldEndNodenewEndNode对比,若相同两个指针都往左移一位,不同则进入下一步
  3. oldStartNodenewEndNode对比,若相同则将oldStartNode移动至结尾,oldStartNode指针右移一位newEndNode指针左移一位,不同则进入下一步
  4. oldEndNodenewStartNode对比,若相同则将oldEndNode移动至开头,oldEndNode指针左移一位newStartNode指针右移一位,不同则结束

注意:以上任意步骤成功指针移动后,将重新开始从第一步开始比较!

移动节点

【Diff算法图解】带你探索React、Vue2.x的Diff算法_第6张图片
以图为例,双端比较的步骤为:

  1. 步骤一进行对比,失败,进入下一步
  2. 步骤二进行对比,失败,进入下一步
  3. 步骤三进行对比,失败,进入下一步
  4. 步骤四进行对比,成功,d节点移动到a节点前,指针移动,重复步骤

【Diff算法图解】带你探索React、Vue2.x的Diff算法_第7张图片

上图为第一次双端比较指针移动后的结果,我们开始第二次双端比较:

  1. 步骤一成功,两个指针右移一位。再次进入步骤一
  2. 步骤一成功,两个指针右移一位。再次进入步骤一
  3. 步骤一成功,对比完成,不需要移动节点。

细心的小伙伴可以发现,我们同样的新旧节点对比,Vue2的diff只需要移动一次节点,但React的diff需要移动三次节点。可以看到双端比较的优点还是非常明显的!

特殊情况

双端比较确实牛逼,但是并不是每一次都能那么顺利的,如果我四个步骤全部失败,我们该怎么去移动节点呢?

【Diff算法图解】带你探索React、Vue2.x的Diff算法_第8张图片
从上图我们可以看到,我们的四次对比全都失败。

那么我们该怎么办呢?我们拿新列表的第一个节点b去旧列表进行进行遍历比较,这里会有两种情况,找到相同节点没找到相同节点

以上图为例,我们先说找到的情况,在旧节点中找到相同节点b,将节点b移动到首位,就像上面的步骤四一样。

【Diff算法图解】带你探索React、Vue2.x的Diff算法_第9张图片
然后重新开始进行双端的步骤对比。

如果我们没有在旧列表里面找到节点呢?就像下面这样
【Diff算法图解】带你探索React、Vue2.x的Diff算法_第10张图片
我们直接在头部添加e节点,然后将newStartNode指针后移
【Diff算法图解】带你探索React、Vue2.x的Diff算法_第11张图片
然后开始进行双端对比!重复之前的步骤

增加节点

我们先来看下图

【Diff算法图解】带你探索React、Vue2.x的Diff算法_第12张图片
可以发现,我们的双端比较一直是在第二步中成功,指针将一直移动,直到下图这样

【Diff算法图解】带你探索React、Vue2.x的Diff算法_第13张图片
可以看到我们的oldEndNode已经移动到了b节点的前面。

此时oldEndNode已经小于了oldStartNode,但是新列表中还有剩余的节点,我们只需要将剩余的节点依次插入到oldStartNode的DOM之前就可以了。为什么是插入oldStartNode之前呢?原因是剩余的节点在新列表的位置是位于oldStartNode之前的,如果剩余节点是在oldStartNode之后,oldStartNode就会先行对比,其实还是与第四步的思路一样。

移除节点

【Diff算法图解】带你探索React、Vue2.x的Diff算法_第14张图片
聪明的同学就可以发现了,这里的情况和增加节点完全是反过来的。

出于流程,我们还是分析一下吧,还是双端比较,指针不停的移动直到下面这样:

【Diff算法图解】带你探索React、Vue2.x的Diff算法_第15张图片
当新列表的newEndIndex小于newStartIndex时,我们将旧列表剩余的节点删除即可。

总结

文章只是大致解释了两种diff算法的思路,详细的步骤还需要大家一起去探索和研究。

你可能感兴趣的:(技术文章,react,vue,前端,vue.js,react.js)