我们都知道,React和Vue框架中都使用了diff算法来优化节点的更新。在Vue中,使用了两对指针并且设置了四种命中策略+map表来实现diff最小化更新的。那么React的diff算法和Vue的diff算法是相同还是有所区别呢?今天我们就来探究一下。
Fiber
首先,我们需要明确react的diff算法的执行时机:diff算法发生在新的jsx产生的ReactElement和旧Fiber树进行对比,生成新Fiber树的过程中。
所以我们先要看看Fiber树到底长什么样。以下代码,其对应的Fiber树就长这样。
编辑
编辑
child指向子节点,reutrn指向父节点,sibling指向后一个兄弟节点。
Diff算法
在diff阶段,会为旧的fiber打上对应的标记,后序再统一处理。例如:Delete标记(需要删除),Placement标记(需要插入)。
和Vue一样,React的也是只进行同层元素的对比。也就是同层元素之和同层元素比。
我们可以从两种情况考虑。
1、更新后只剩下一个节点
如果更新后同层只剩下一个节点,会与更新前的节点进行对比。会先比较key再比较type,只有key相同才会比较type
- 如果key不同,就会就将旧的fiber标记为delete,然后与其sibling再进行对比。最后如果不能复用,则创建新的节点。
为什么还要和他的sibling比较呢?可以考虑以下情况:
更新前: 更新后
<li key="a">a</li> <li key="c">c</li>
<li key="b">b</li>
<li key="c">c</li>
<li key="d">d</li>
可以看到即使第一个的key与更新后的key不匹配,其兄弟节点还有可能与其匹配,所以还要继续尝试匹配其兄弟节点。
- 如果type不同,此时表明key已经相同了。就会直接将当前节点和所有它的sibling标记为删除,创建新的节点。
这里为什么type不同就不继续尝试匹配它的兄弟节点呢?
因为上面说了,react是先匹配key,key相同再匹配type的。能比较出type不同,说明key一定相同。又因为key是唯一的,所以其兄弟节点一定匹配不上。
2、更新后有多个节点。
如果更新后同层有多个节点。有以下三种情况:
- 节点内容更新
- 节点减少、新增
- 节点位置变化
react会将新的ReactElment组织成一个newElement数组。按照更新,新增/减少、位置变化这样的优先级处理。
由于react在16版本的时候,将虚拟dom更换为了fiber架构。所以diff算法两边对比的一边是中间由sibling指针连接的类似链表的Fiber结构、一边是数组,所以不能像Vue一样使用两对指针。所以react采用的是另外一种方法。
react会进行两轮遍历。
-
第一轮遍历处理内容更新的情况:遍历newChildren数组,对比newChildren[i]和旧的Fiber,判断是否可以复用。
- 如果可以复用则指针往下i++,判断下一个旧的Fiber是否可以复用,可复用的话就会去直接用旧的Fiber。
- 如果不可复用:会先判断key再判断type。如果key不同,就会马上跳出第一轮遍历,因为这不属于内容更新。如果只是type不同,会将旧的Fiber标记为Delete,并且创建一个新的Fiber,然后为新的Fiber标记为Placement,代表该节点需要插入。
跳出第一轮遍历可以有两种情况:
- 节点key不同,直接跳出。
- newChildren数组或者旧的Fiber链表其中一个遍历完。
-
第二轮处理另外两种情况:
1、newChildren和旧的Fiber链表同时遍历完,代表所有节点可以复用。
2、newChildren先遍历完,代表旧的Fiber链表中没遍历到的元素需要删除,所有没遍历到的元素打上Delete。
3、旧的Fiber链表先遍历完,代表newChildren中没遍历到的元素都需要添加,新生成的fiber都打上Placement。
还有第四种情况:newChildren和current Fiber都没有遍历完成。这就对应了key不同跳出第一轮循环的情况,这种情况下有可能是节点移动了或被删除了。
所以React建立了一个哈希表(以旧Fiber的key为键,以旧的Fiber为值),通过key来寻找oldFiber。
通过key找到oldFiber之后,会有两个索引newChildren数组中的lastPlaceIndex(newChild中最后一个可复用节点的index)和 旧Fiber树中的oldIndex(current Fiber树中最后一个可复用的Fiber)这两个index在第一轮遍历完成之后肯定是相等的。
通过key找到oldFiber后,oldIndex会被赋值为fiber的在旧序列中的index。
此时:
1、如果oldIndex < lastPlacementIndex,证明这个节点肯定要被往下移。标记fiber为placement
2、如果oldIndex ≥ lastPlacementIndex,将oldIndex赋值为lastPlaceIndex,说明如果后面再在oldFiber中匹配索引到小于lastPlaceIndex的fiber,就要往下移。
请看下面例子:
编辑
第一轮循环,oldIndex和lastPlaceIndex都为0,A(旧)与A(新)匹配,两个指针都往后移。
编辑
此时B(旧)和C(新)的key不匹配。跳出第一轮循环。此时oldIndex = 1、lastPlaceIndex = 1。进入第二轮循环。
第二轮循环会直接从哈希表中去寻找C这个节点。找到C在原来的列表中index为2,将oldIndex赋值为2。
编辑
此时oldIndex = 2 > lastPlaceIndex = 1。将lastPlaceIndex赋值为oldIndex,然后继续匹配下一个。这其实就像C(旧)在和B(旧)说我已经排到新列表中了,你在我前面,后面再匹配到你,你要到我后面去。
那在newChildren列表中匹配到b后,是如何知道B(旧)在C(旧)前面的呢?就是通过oldIndex和lastPlaceIndex的比较。可以接着往下看。。。
编辑
此时一看lastPlaceIndex是2,oldIndex是1。lastPlaceIndex是上一次匹配C(旧)的index,oldIndex是B(旧)的index。这就看出了B(旧)在C(旧)的前面。而现在要更新为B(新)在C(新)后面。所以B(旧)需要移动。这就是为什么oldIndex < lastPlaceIndex时需要打上Placement。
接下来就是D(旧)和D(新)匹配,直接复用。
编辑
处理完的Fiber就会被从map中删去,然后最后剩下的map中的Fiber都会被打上Delete。
至此,diff算法大致流程就已经讲完了。