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节点之前。全程不需要更新节点了,因为在遍历旧节点时更新过了
参考:
《Vue.js设计与实现》