React和Vue的diff算法对比

276 阅读5分钟

react和vue的diff算法的核心思路有三点:

1、只对比同一层级的节点

2、新老节点类型不同,先删除再新建

3、引入key作为节点的唯一标识,提高diff效率

react diff算法:

因为节点发生了变化一般是内容发生变化,而第一轮遍历就可以解决大部分问题了。所以设计了旧fiber树和新虚拟dom树会进行两次遍历:

第一轮遍历

找出可以复用的节点。通过对比key值以及type值,如果key相同,type不同,那么旧fiber节点会打上deletion的标签,然后在新fiber树新增当前type的节点,打上placement标签。如果两者都相等则说明该节点可以打完补丁直接复用到新fiber树中。如果遍历到新旧节点的key值不同时,记录当前新节点的索引值,称为协调点。第一轮遍历结束。

第二轮遍历

处理不能复用的节点。react会生成一个map存放旧fiber树未遍历的节点信息,从上一轮的协调点开始继续遍历新虚拟dom树。如果当前vnode可以在map中找到对应的旧fiber节点,说明当前旧fiber节点可以复用,那么会直接在新fiber树复用该节点并插入,并在map中删除旧fiber节点的信息。接着判断旧fiber节点在原数组的索引值,如果小于协调点的话,说明当前节点需要移动,那么新fiber树上的节点会打上placement标签。

如果当前vnode在map中没找到对应的旧fiber节点,那么直接生成新fiber节点,并打上placement标签。

如果vnode遍历完了,那么将map中剩余的旧fiber节点都打上deletion标签。

commit

当新fiber树构建完成后,此时render阶段(可中断)结束,进入commit阶段(不可中断)。首先是删除旧fiber树中的deletions数组中fiber对应的dom节点,接着处理新fiber中标记了placement标签的节点,将节点移动到往后第一个没有placement标签的fiber节点的dom节点后面。最后再对新fiber节点的dom进行更新操作。

vue2双端diff算法:

双端diff是新vdom树和旧vdom,也就是新旧vnode进行对比。

对于同样的更新场景,双端diff算法执行的dom移动操作次数更少。本质是双指针,分别用newStartIndex和newEndIndex指向新虚拟dom树的头尾节点,再用oldStartIndex和oldEndIndex指向旧虚拟dom树的头尾节点。

第一轮遍历

进行四次key值对比,头跟头,尾跟尾,头跟尾,尾跟头:

如果头跟头或者尾跟尾key相同,那么说明当前的端点节点可以直接复用,新旧vdom树的头尾指针都向中间移。

如果头跟尾key相同,即当newStartIndex和oldEndIndex相同时,那么oldEndIndex对应的dom节点会插入到oldStartIndex对应的dom节点之前。

如果尾跟头key相同,即当newEndIndex和oldStartIndex相同时,那么

oldStartIndex对应的dom节点会插入到oldEndIndex对应的dom节点之后。

四次对比均未命中,也就是双端对比启动不了。vue2自动创建一个oldKeyToIdx Map(和react相似),存放旧节点的节点信息。以newStartIndex指向的节点为参照,遍历map:

如果找到key值相同的节点,并且type相同,那么旧节点对应的dom节点会插入到oldStartIndex对应的dom节点之前,newStartIndex++,旧节点置为undefined

如果key值相同,但type不同,创建一个新的dom节点插入到oldStartIndex对应的dom节点之前,newStartIndex++,旧节点置为undefined

如果没找到key值相同的节点,根据新vnode创建一个新的dom节点插入到oldStartIndex对应的dom节点之前,newStartIndex++

之后再开启双端对比。

接着进入第二轮遍历

如果出现oldEndIndex>oldStartIndex,说明还剩下新节点[newStartIndex,newEndIndex],那么遍历新节点,根据新vnode创建对应dom的节点,依次挂载到父级dom节点的最后

如果出现newEndIndex>newStartIndex,说明还剩下旧节点[oldStartIndex,oldEndIndex],遍历旧节点依次卸载

react为什么不用双端diff算法

因为在同一层级的fiber节点没有反向的指针,sibling是单向,只能从左到右遍历,所以双端diff实现不了。

vue3 快速的diff算法:

快速diff也是新vdom树和旧vdom,也就是新旧vnode进行对比。

第一轮遍历

利用双指针向中间遍历,预处理相同的前置节点和后置节点,直到遇到不相同的节点。

第二轮遍历

只剩下新子节点,依次创建新的dom节点,并插入到尾部已处理的节点之前。

只剩下旧子节点,依次从父级DOM节点中删除对应的dom节点

新旧子节点都有剩余,创建一个newIndexToOldIndexMap数组,存放的是新子节点在旧vdom树的位置,数组元素默认值为0,所以在映射时所有值都必须+1来空出0下标这个位置。

顺序遍历旧子节点:

如果当前旧子节点找到了对应的新子节点,那么就会更新newIndexToOldIndexMap数组的值,并更新当前节点的内容。在填充newIndexToOldIndexMap数组的过程中,也会判断节点是否需要移动(和react diff判断调和值原理差不多),因为如果不需要移动的话后续就不需要计算最长递增子序列了。最后会删除那些找不到新子节点的旧子节点。计算最长递增子序列数组:increasingNewIndexSequence(顺序索引)

逆序遍历新子节点:

首先判断newIndexToOldIndexMap数组的值是否为0,如果为0那么直接新建dom节点,插入到下一个新子节点对应的dom节点之前;

接着判断当前节点的索引值存在于increasingNewIndexSequence数组中,如果存在则说明不需要移动;

如果不存在于数组中,当前新节点的dom需要移动到下一个新子节点对应的dom节点之前。全程不需要更新节点了,因为在遍历旧节点时更新过了

参考:

juejin.cn/post/716106…

《Vue.js设计与实现》