likes
comments
collection

[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers

作者站长头像
站长
· 阅读数 12

React Diff -- Reconcile

希望可以帮助到,与我同样纠结在此的前端程序员们。

[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers

大家都知道,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

下面是reconcile构建的过程源码

[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers

这里我们只是简单聊聊reconcile阶段的初次渲染,暂时不涉及到React的schedulercommitWork阶段。有人可能会问,文章到了这里没出现Diff的过程,那么初次渲染和Diff有什么关系呢?其实答案很简单,大家也都知道,有了初次的渲染,我们下次更新才有Diff的对象呀! 接下来我们就来和大家聊聊更新阶段的Diff

更新Diff渲染

我们先对更新Diff渲染做总结,之后看代码会很有感觉:React的更新Diff渲染在reconcile阶段,主要的任务是将不同类别(type)旧的fiber节点和新的vdom结构进行Diff,可以分为:对普通文本,数字节点进行reconcile,对单节点进行reconcile,对多节点进行reconcile

[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers

普通string, number节点的 reconcile

[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers

  • 进入这个函数,已经确定newChildren是文本节点了。

    • 这里判断oldFiber是不是文本节点。

    • 如果是文本节点,可以复用节点,就使用useFiber来复用创建新的文本fiber节点。

    • 如果不是文本节点,不可以复用节点,删除当前层级的oldFiber链表,只创建一个新的文本fiber节点。

单节点的 reconcile

[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers

  • 进入这个函数,已经确定newChildren是单节点了。
    • 这个时候判断oldFiberkeytype,看看可不可以复用。

    • 可以复用,删除oldFiber链表剩余的链表节点。

    • 不可以复用,删除oldFiber链表的全部节点,创建新的fiber节点。

多节点的 reconcile

多节点是Diff的精髓,我们还是先总结,等一下看代码会非常有感觉,多节点的diff的主要流程是:三次循环遍历,一次判断,完成oldFiber链表和新的vdom结构的diff,从而完成oldfiber链表节点的删除和移动和newfiber节点的增加,以构造新的fiber结构

第一次循环遍历

[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers

  • 如果第一次循环遍历的顺利的话,大家看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节点,flagPLACEMENT
      • 如果newChldrenmap当中找到对应keyfiber了,说明可以复用oldfiber节点。
      • 复用分为两种:一种是移动复用,一种是更新复用。
      • 如果是更新复用:那么就updateElement(oldChildrenVdom, newChildrenVdom)复用构建fiber节点,然后在map当中删除复用的oldFiber
      • 如果是移动复用:那么就利用placedIndex索引来移动节点,flag is MOVE。这里我当时最大的疑问就是怎么样通过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

  • 和上文第一次判断那里说的一样,当newFiber链表已经全部顺利遍历完成,oldFiber链表还有节点的时候,删除剩下的oldFiber节点。

第二次循环遍历

[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers

  • 第二次循环遍历的两个作用,我们在文章当中已经全部展示了。
    • 作用1:文章开头说的初次渲染。

    • 作用2:第一次循环遍历顺利的时候,当oldFiber链表节点已经遍历完成后,newFiber还有的时候,继续遍历newFiber来创建fiber,建立联系。

第三次循环遍历

[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers

  • 上文从代码的角度聊了节点的移动策略,下面我们以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过程是一层一层的oldFibernewFiber去diff。
  • 移动不是dom节点的平移,本质上还是复用,只不过不用重新document.createElement(),利用fiber结构上的引用stateNode去将真实DOM,去insertBefore或者appendChild来完成的移动。