[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers
React Diff -- Reconcile
希望可以帮助到,与我同样纠结在此的前端程序员们。
![[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers](https://static.blogweb.cn/article/137be404ff764f23a8ef9ecb69a63d17.webp)
大家都知道,React真正的Diff逻辑是在reconcileChildFibers,这没有任何问题,那么图片当中的两行代码是什么?与Diff有什么关系,事实上 reconcileChildFibers 被当作闭包函数返回了出来,被React用于两个方面,第一个方面是初次挂载渲染,第二个方面是更新Diff渲染。那我们接下来就从Reconcile阶段的初次渲染和更新Diff两个方面来说React Diff。
初次渲染
我们先对初次渲染做总结,之后看代码会很有感觉:React的初次渲染在reconcile阶段,主要的任务是从根节点FiberRoot开始深度优先遍历多叉树结构的vdom,映射构建单层fiber链表结构和整个fiber多叉树结构(及构建fiber对象的属性,包括但不限于index, key, type,internate, flags……),并且构建fiber结构的child(第一个孩子), return(父亲), sibling(兄弟)关系。
![[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers](https://static.blogweb.cn/article/abdc6bc07db4422e851fc6cda22067b9.webp)
下面是reconcile构建的过程源码
![[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers](https://static.blogweb.cn/article/28f91ae03a244dfdb1de68500d5199cb.webp)
这里我们只是简单聊聊reconcile阶段的初次渲染,暂时不涉及到React的scheduler和commitWork阶段。有人可能会问,文章到了这里没出现Diff的过程,那么初次渲染和Diff有什么关系呢?其实答案很简单,大家也都知道,有了初次的渲染,我们下次更新才有Diff的对象呀! 接下来我们就来和大家聊聊更新阶段的Diff。
更新Diff渲染
我们先对更新Diff渲染做总结,之后看代码会很有感觉:React的更新Diff渲染在reconcile阶段,主要的任务是将不同类别(type)旧的fiber节点和新的vdom结构进行Diff,可以分为:对普通文本,数字节点进行reconcile,对单节点进行reconcile,对多节点进行reconcile
![[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers](https://static.blogweb.cn/article/8b67a876ffd9432099cb7dfe39769bb2.webp)
普通string, number节点的 reconcile
![[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers](https://static.blogweb.cn/article/8d86fb0a63354e35a351b49715e6cadc.webp)
-
进入这个函数,已经确定
newChildren是文本节点了。-
这里判断
oldFiber是不是文本节点。 -
如果是文本节点,可以复用节点,就使用
useFiber来复用创建新的文本fiber节点。 -
如果不是文本节点,不可以复用节点,删除当前层级的
oldFiber链表,只创建一个新的文本fiber节点。
-
单节点的 reconcile
![[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers](https://static.blogweb.cn/article/ff6d3fd29d5c406f86b29274b3cabdee.webp)
- 进入这个函数,已经确定
newChildren是单节点了。-
这个时候判断
oldFiber的key和type,看看可不可以复用。 -
可以复用,删除
oldFiber链表剩余的链表节点。 -
不可以复用,删除
oldFiber链表的全部节点,创建新的fiber节点。
-
多节点的 reconcile
多节点是Diff的精髓,我们还是先总结,等一下看代码会非常有感觉,多节点的diff的主要流程是:三次循环遍历,一次判断,完成oldFiber链表和新的vdom结构的diff,从而完成oldfiber链表节点的删除和移动和newfiber节点的增加,以构造新的fiber结构。
第一次循环遍历
![[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers](https://static.blogweb.cn/article/7c131e6354e34cdf9e93bfd065f4a536.webp)
-
如果第一次循环遍历的顺利的话,大家看for循环的退出条件,也就是
newChldren都可以复用oldFIber链表节点或者是oldFiber有的链表节点,newChldren都可以复用。那么newChldren就可以顺利建立起child, sibling的联系,所以在顺利的情况下,第一次循环退出的条件要不就是newChldren的长度太长了,要不就是oldFiber的链表长度太长了。- 如果是
oldFiber的链表长度太长了,经过第一次判断,会delete掉多余的oldFiber链表节点。 - 如果是
newChldren的长度太长了,在经过第二次循环遍历会构建没有遍历到的newChldren链表节点。 - 自此结束,如果顺利的话不会进入第三次循环。
- 如果是
-
如果第一次遍历循环不顺利的话,即只要发现一个不能复用的节点,就立马退出第一次循环。从不能复用的节点开始,将所有的
oldfiber的链表节点,依次加入到map结构当中。-
然后进入第三次循环,继续遍历
newChldren链表,遍历到一个newChldren, jsx节点,就拿着key去由oldFiber节点构成的map当中去找 可以复用的key。- 没有找到,构建新的
fiber节点,flag为PLACEMENT。 - 如果
newChldren在map当中找到对应key的fiber了,说明可以复用oldfiber节点。 - 复用分为两种:一种是移动复用,一种是更新复用。
- 如果是更新复用:那么就
updateElement(oldChildrenVdom, newChildrenVdom)复用构建fiber节点,然后在map当中删除复用的oldFiber。 - 如果是移动复用:那么就利用
placedIndex索引来移动节点,flagisMOVE。这里我当时最大的疑问就是怎么样通过placedIndex来移动的。源码当中的index < placedIndex为什么会被标注为移动? 原因很简单就是两个节点的相对位置发送了变化,比如原来是a -> d现在在是d -> a, 我们从现在的d -> a开始出发遍历,遍历到d,发现在原来的1位置,此时placeIndex更新为1,当遍历到a的时候,我们发现a在原来的0位置,如果相对位置没有发送变化,这里a的位置应该在b的后面。大于1 (placeIdex),但是现在是< 1意味着a到了b前面,所以相对位置发送了变化,标记为移动。然后在map当中删除复用的oldFiber。之后删除map当中剩余的fiber节点。 - 并循环移动和更新来构建出这一层新的
fiber结构。
- 没有找到,构建新的
-
一次判断
![[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers](https://static.blogweb.cn/article/fb4ca7a6b52c4499b6976ae7294c0c53.webp)
- 和上文第一次判断那里说的一样,当
newFiber链表已经全部顺利遍历完成,oldFiber链表还有节点的时候,删除剩下的oldFiber节点。
第二次循环遍历
![[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers](https://static.blogweb.cn/article/f291464ed063401f9c758375a6bc44b7.webp)
- 第二次循环遍历的两个作用,我们在文章当中已经全部展示了。
-
作用1:文章开头说的初次渲染。
-
作用2:第一次循环遍历顺利的时候,当
oldFiber链表节点已经遍历完成后,newFiber还有的时候,继续遍历newFiber来创建fiber,建立联系。
-
第三次循环遍历
![[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers](https://static.blogweb.cn/article/0d88d575b857430396734d3e2ebcbb1c.webp)
- 上文从代码的角度聊了节点的移动策略,下面我们以dom的角度来表达移动diff的过程。
旧节点 a -> b -> c -> d -> e -> f (key为自己)
新节点 a -> c -> b -> e -> g (key为自己)
遍历新节点到c发现,b不能符合立马跳出第一次循环,将剩下的旧节点放到了map当中(key为item)。 {"b": b, "c": c, "d": d, "e": e, "f": f}, 遍历新节点c去map当中去找,找到了 placeIndex =2, 更新节节点不移动,删除map当中的c,遍历到b找到了,index < placeIndx -> 1 < 2 ,所以标记b为移动节点,同理e更新,g在map当中找不到,标记为增加。
遍历完之后要去更新dom了,这里要注意,先删除真实dom节点中要移动和删除的节点,删除之后真实dom变为 a -> c -> e,这时b移动的时候,索引为原来的mountIndex 为2,判断2上有没有元素,这里a -> c -> e 索引为2是e,所以执行parentDom.insertBefore() 将b插入到e之前,完成移动,执行到g新增的时候,索引为原来的mountIndex为4,判断4上有没有元素,这里a -> c -> b -> e,索引为4没有执行parentDom.appendChild(), 完成新增,最终结果 a -> c -> b -> e -> g,移动,更新完毕。
一点总结
我的两点理解,希望可以帮助到与我同样纠结在此的前端程序员们
diff过程是一层一层的oldFiber和newFiber去diff。移动不是dom节点的平移,本质上还是复用,只不过不用重新document.createElement(),利用fiber结构上的引用stateNode去将真实DOM,去insertBefore或者appendChild来完成的移动。
转载自:https://juejin.cn/post/7149818538155474951