全文所有算法,都只针对新旧两棵树的某一级。
在React中,是diff旧FiberNode树中某一级的单向链表,和新JSX树中某一级Children数组。
求最长递增子序列
理解最长递增子序列,对理解Diff算法很重要。
React 和 Vue3 的快速排序,都用到了最长递增子序列。
不过,Vue3的快速排序中明确的进行了求最长递增子序列的操作React则只是在遍历过程中,用一个变量,记录索引位置,来判断当前遍历到的节点是否在最长递增子序列中。
求解过程:
- 创建一个数组,数组中的每一项代表一个新节点。每一项的值代表这个节点在旧节点中的位置。
- 求这个数组的最长递增子序列。
- 那么,这个子序列所代表的那些节点,在更新前后,位置的先后关系,没有变化。
- 于是只需要移动或者删除剩下的节点即可。
React
React的diff算法,是对比当前的fiberNode树,和新生成的JSX树。 为方便行文,下文都统称为旧节点 和新节点
React的diff算法分为
- 单节点Diff: 新节点为单节点
- 多节点Diff: 新节点为多节点
单节点Diff
这种情况比较简单。
遍历旧节点
- 找到key和type相同的,复用旧节点创建新的fiberNode
- 对key或者Type不同的旧fiberNode标记删除
- 如果没有找到可复用的旧节点,则标记删除所有旧节点,标记挂载新节点
多节点Diff
有一个前提,大部分情况下节点的位置不会改变。
所以会首先处理位置没有改变的情况。
会遍历两遍。
第一遍找到位置没变的所有节点
第二遍处理其他位置改变了的节点。
第一遍:
同时遍历新旧节点。
声明一个变量:lastPlacedIndex = 0;
从头,也就是索引为0处,开始遍历。 检查新旧节点,相同索引处,节点是否可以复用。
每次循环会更新lastPlacedIndex变量。
- 如果可以复用。 lastPlacedIndex++;
- 如果key不同,会直接结束第一遍循环
- 如果key相同但是type不同,会标记旧fiberNode为DELETION,然后继续循环。
第二遍:
四种情况
- 新旧节点都遍历完了。 diff结束。
- 旧节点有剩余,新节点遍历完了。标记删除所有剩下的旧节点。
- 旧节点遍历完了,新节点还有剩余。新增所有剩余新节点。
- 都没有遍历完。这时要处理节点移动。
情况四
最复杂的是情况四
首先用一个键值对,把所有就节点按key保存下来。
然后,遍历新节点。找到每个新节点是否有对应的旧节点。
如果没有:则新增这个节点
如果有:
找到这个旧节点的索引值。 这个索引值代表新节点在旧节点中的位置。
-
如果这个索引值大于或等于lastPlaceIndex则不需要移动。 并更新lastPlacedIndex为这个索引值。
-
如果小于lastPlacedIndex, 则代表需要移动。
这里乍看之下很难理解。 其实这是在求一个递增子序列。 如果在上面这一系列操作中,如果某个新节点在旧节点中的索引,大于lastPlacedIndex。这些大于lastPlacedIndex的节点们,在更新前后的顺序没有变化。 所以不需要移动他们,而应该去移动那些位置发生过变化的节点。 这个思想在Vue3的快速Diff算法中也有应用。
Vue
在Vue中,diff算法对比的两边都是虚拟Dom。
简单 diff 算法
双循环遍历新旧节点。
外层循环是新节点 记作 new,
内层循环是旧节点:记作 old
假设当前外层循环,循环到了第i次
步骤
声明一个变量: lastIndex = 0;
这里使用到的思想和React diff算法一样。
- 拿到 new[i].key,在内层循环中找 old 中相同 key 的索引:index
- 如果 index>= lastIndex. 代表 new[i]不需要移动
- 如果 index< lastIndex. 代表 new[i]需要移动。
- 移动到 new[i-1]后面。
- 如果 index 不存在的。代表 new[i]是新增的节点.
- 挂载到 new[i-1]后面
- 此时双循环遍历完毕。
- 遍历 old,去 new 中找对应的 key 是否存在
- 如果不存在,代表 old[i]需要被卸载。
双端 diff
从新旧节点,两条链的两端同时开始比较。
每循环完一次,都假设被匹配上处理过之后的节点不存在
于是,可一系列重复的子问题,类似高中的数学归纳法,一个复杂的大问题分解成一步一步,最后只是对比新旧两条链,各两个节点的问题
声明四个指针,指向新旧两条链的两端。
每次循环,都会两两对比,这四个指针所指向的节点。
如果指向头部的指针匹配到了对应的节点,则头部指针向尾部移动一个位置
如果指向尾部的指针匹配到了对应的节点,则尾部指针向头部移动一个位置
结束条件:两条链都头 指针的索引 > 尾 指针的索引 了
会依次对比这四个:
新头和旧头
无需移动
如果匹配上,更新 dom 节点。
新尾和旧尾
无需移动
如果匹配上,更新 dom 节点
新尾和旧头
如果匹配上,需要移动
把旧头移动到旧尾的后面
新头和旧尾
如果匹配上,需要移动
把旧尾移动到旧头的前面
如果以上4个条件都没满足
拿新头去旧链中去找key相同的节点
- 如果找到
- 把旧链中找到的节点对应的dom移动到,旧头之前
- 把旧链中这个索引处置为undefined
- 以后循环的时候,如果旧头是undefined,则直接进行下一次循环
- 如果没找到
- 代表这是一个新节点。直接挂载到旧头之前
如果循环结束后还有节点遗漏
-
新链中还有节点未被处理。则挂载这些节点
-
旧链中还有节点未被处理,则卸载这些节点
快速diff
处理相同的前置和后置元素
先从两端遍历新旧两条链,把两端位置没有变的节点都找出来,更新他们。
然后进入下一步,处理两条链剩余的部分的移动,挂载和卸载
移动,挂载和卸载
-
把新链中的剩余节点在旧链中的索引,保存成一个数组:source。默认值为-1,代表是新节点需要被挂载。
-
求这个数组的最长递增子序
source数组中每一项代表的节点,在新链中的索引的递增的
最长递增子序中每一项代表的节点,在旧链中也是递增的。
那么:,最长递增子序列中每一项代表的节点的顺序,在更新前后没有变化
那么:只需要移动其他节点的位置即可
-
遍历新链,找到所有不在最长递增子序列中的节点,把这些节点移动到在新链中对应的索引位置处即可
把i节点,移动或创建到i+1节点之前。如果i+1节点不存在,就放到父容器尾部。