react 中的 diff 算法,通俗易懂的解读
react 中的 diff 算法,通俗易懂的解读
diff 算法在前端面试中也算是一个高频考题了,那怎么给面试官一个满分解答呢?难道还是简单的说个“深度优先,同层级比较”吗?这太短小精悍了......!
好了,下面开始进入正题
单节点 diff
单节点 diff 就比较简单了,从同层级的老fiber
节点中找出key
值和type
都相等的老节点,如果该老 fiber 节点存在,则复用他,然后删除剩余的节点,否则重新生成一个新的fiber
节点(这也就意味着以这个节点为根的子树需要重新生成)。
下面我们来看看单节点diff
的核心源码
相关参考视频讲解:进入学习
多节点 diff
重点来了,注意了啊,多节点才是精髓
多节点 diff 主要可以分为两轮循环,第一轮循环主要是新旧节点同位置对比,找到第一个无法复用的节点位置后,以最后一个可复用旧节点的位置作为后续作为判断节点是否需要重新插入的基准位置值(该值后续可能会变),然后跳出循环。
如果经历了第一轮循环后,会存在三种情况:
新节点已经遍历完成:删除剩余的老节点,结束多节点 diff
老节点遍历完成,新节点还为遍历完,将剩余的新节点逐一创建
fiber
节点,并标记为重新插入,然后结束 diff老节点、新节点都没遍历完,这种情况就比较复杂,需要将剩余的老节点放入一个 map 中,然后开启第二轮循环;
第二轮循环:
本轮循环是遍历剩余的新节点,遍历时,新节点都从 map 中寻找有没有自己能复用的老节点(key 和 type 相同即可复用),如果 map 中存在就复用,然后将该老节点从 map 中移除,否则就重新生成。如果老节点被复用了,就会将该老节点原来所在的位置和第一轮循环确定的基准位置值比较,老节点的位置在基准位置值的左边时,说明复用该老节点的新节点需要重新插入,基准位置值不变;老节点的位置在基准位置值的右边时,说明复用该老节点的新节点无须移动,但是基准位置值需要更新为老节点的位置。
第二轮循环结束了,只需要将 map 中剩余的老节点标记为删除即可。
举个例子:(第三种情况)
目前页面上有 3 个 li,内容和 key 分别为 1、2、3(fiber
树如下图所示)
现在我要让页面变成 3 个 li,内容和 key 分别是 1、3、2,其所对应的ReactElement
结构为
那么我们需要怎么做呢?
注意:diff 比较是老
fiber
与newChild
(在这我就先把它叫新节点吧)的比较
首先定义一个
lastPlacedIndex
用来作为基准位置来判断旧节点是否需要移动,初始为 0,newIndex 代码新节点的索引此时第一轮循环开始了,oldFiber 跟 newChild 进行比较,发现它们的 key 和 type 都相等,该节点可复用,而且 oldFiber 的位置并不在 lastPlaceIndex 的左边,无需重新插入,继续往下遍历
这个时候发现 oldFiber 的 key 值和 newChild 的 key 不想等,这意味着无法复用,第一轮循环结束。老节点和新节点都未遍历完成,需要开启第二轮循环了。
将剩余的老节点存入一个 map 中,如果老节点中存在 key 值,则将该 key 值作为 map 中的 key,没有就以老节点所在的位置作为 map 中的 key,该节点作为 map 中的值
第二轮循环开始,新节点存在 key 值,为 3,从 map 中寻在以 key 为 3 的值,发现 map 中存在该值,且该老节点的 key 和 type 都 newChild 相同,可以复用,然后将该老节点的位置(2)与基准位置值(0)对比,发现该老节点位置在基准位置的右边,复用该老节点的新节点无须移动,基准位置值更新为 2,最后从 map 中删除 key 为 3 的值
新节点存在 key 值,为 3,从 map 中寻在以 key 为 2 的值,发现 map 中存在该值,且该老节点的 key 和 type 都 newChild 相同,可以复用,然后将该老节点的位置(1)与基准位置值(2)对比,发现该老节点位置在基准位置的左边,复用该老节点的新节点需要重新插入,标记为 Placement,基准位置值不变,最后从 map 中删除 key 为 2 的值
此时新节点也已经遍历完成了,第二轮循环结束,将 map 中剩余的老节点标记为删除
下面来看下 react diff 代码片段的实现
留个思考题?如果 diff 过程中,oldFibers 中有部分节点的 key 值相同,会造成什么问题呢?
评论