[React Origin Code] 2022年来聊聊React Update-- 双缓存Fiber树

3,916 阅读5分钟

React Update-- 双缓存Fiber树

学习源码不易,和大家一起共勉!

前文: [React Origin Code] 2022年来聊聊React Mount-- renderRootSync当中,我们刚刚说完 React Mount,今天我们继续来聊聊React update,在聊update之前,我们还需要再次回到mount阶段,这次主要把握和关注两个概念,workInProgress树current树的双缓冲Fiber机制。这在React update阶段起到了至关重要的作用。

关于双缓存树的资料

双缓存

React 双缓存Fiber树

下面我们以React 计数器案例来聊聊React update逻辑

image.png

图一:计数器案例来说明React update逻辑

createWorkInProgress

下面这段总结读完本文,可以回过头来看看:

createWorkInProgress函数mount的时候只会走一次,就是创建workInProgress树的根节点,mount的其他节点不进到createWorkInProgress函数,因为此时没有currentfiber树,其他节点的current === null,当执行到reconcileChildren函数的时候,走的是mountChildFibers,只有更新的时候,current !== null ,走reconcileChildrenFibers的时候,才让每个子节点先去递归执行createWorkProgress,复用建立workInProgressfiber节点,最终建立好workInProgress树current树,并且用alternate属性,对两棵树的节点进行联系。

image-20221008153300672.png

图二:不同的reconcilChildren逻辑

beginwork前的fiber树

关于mount阶段, beginwork函数的作用和流程,我们在前文已经详细聊过了, 在当前workInProgress fiber节点bedginwork之前,会调用createWorkInProgress函数

mount的时候,在刚进入createWorkInProgreess的函数上打上断点,打印fiber树。结果是这样的。

image-20221009084003763.png

图三:monunt 刚进入createWorkInProgreess的函数的打印结果

6dbd2c1abd78c494b3a785a794706ac.jpg

图四:mount 刚进入createWorkInProgreess的currentFiber树

mount的时候,执行的第一次也是最后一次的createWorkInProgreess函数结果是这样的

1ef6f5afa126949a8e1a3ef2bdd166a.jpg

图五:mount的时候,执行的第一次也是最后一次的createWorkInProgreess后的Fiber树

然后从workProgreess树的根fiber节点开始 mount,关于mount的流程这篇文章很详细,mount之后结果是这样的。

dd37ff8ee17590866bea8abfcf5a6a1.jpg

图六:mount fiber树之后结果

细节注意:下一次更新之前,刚刚 mount时的workInProgress树会被变成current树,这里的变,也仅仅时需要改变FiberRootNode.current指针而已。这也是双缓存机制的魅力所在。

第一次触发计数器 + 1 -> update

mount结束后,我们触发计数器+1, 开始第一次更新,我们由上图可以知道,mount之后,第一次更新之前只有根节点有自己的alternate,所以在createWorkInProgress函数时,根节点fiber可以复用current树的 fiber,由于复用current fiber之后其child也被复用,所以结果是这样的。

7625872d6cfb424e85f9925571cc107.jpg

第一次更新,除了根节点FiberRootNode之外其余节点curretn.alternatte都是null,所以每次到createWorkInProgress函数, 都会通过createFiber去创建新的workProgress节点,当前workInprogressfiber就会和 current fiber建立alternate的相互链接。然后去递归beginworkreconcileChildren,然后再让children fiber去依次按照child -> sibling的顺序去createWorkInProgress。从而通过alternate属性完整建立起workInProgress树current树的联系。

mount和第一次更新还有一个区别,第一次更新reconcileChildren也进入了不一样的逻辑,这一次current fiber节点不再是null了,会进入reconcileChildrenFibers而不是 mountChildrenFibers这时shouldTraceEffect 会变为true给一些需要新插入的节点打上effectTag Placement, Delete…….

最终创建建立双缓存Fiber树的结果是这样的:

eb2a30f9255659c04e768dab95b3a2e.jpg

第一次更新时,beginWork还有一个优化逻辑,workInprogressfiber会和 current fiber 通过新旧props 对比,新旧type对比,以及是否有context的改变,如果没有都没有改变,则如果命中了这条优化逻辑,那么就会走bailoutOnAlreadyFinishWork逻辑,会执行cloneChildFibers返回子fiber。如果没有命中,继续根据不同的typeupdatae不同类型的标签组件。

image-20221009090344140.png

第一次更新时,beginWork还有一个优化逻辑 bailoutOnAlreadyFinishWork

completework

completeworkbeginWork深度优先遍历的回溯阶段,mount阶段的completework已经在前文聊过,这里我们不做赘述。我们这里主要来聊聊update阶段,update时会通过diffProperties对新旧props进行diff, 删除,增加或者来收集变化属性到workInProgress fiber节点的updateQueue数组当中,第i属性为变化的属性,i + 1属性是属性变化的值并且将需要更新属性的节点打上effectTag -> update,最后回溯的过程当中将所有打上effectTagfiber节点收集到effectList链表当中。

最终当 complete 回溯到根节点的时候,从根节点出发的firstEffect属性 就指向effectList链表当中需要commitfiber,当前fibernext是下一个需要commit的节点,依次类推,一直到lastEffect结束。

最后形成的effectList链表是这样的

535922357a9474f4736b52aaabf1c78.jpg

最终形成的effectList链表

最后`commitWork` 根节点。进入`commit`阶段更新渲染视图。

建立effectList链表的好处是不用再像renderer阶段一样,每次无论是mount,还是更新都要从FiberRootNode根节点开始深度优先遍历。有了effectList链表,在commit的阶段,就只对需要更新的fiber进行commit来更新渲染视图。不用去深度优先遍历,回溯的时候去appendChild

第二次触发计数器 + 1 -> update

eb2a30f9255659c04e768dab95b3a2e.jpg

第二次更新前的Fiber树

第二次更新很关键,因为这时内存当中存在两颗react fiber树, 有了这两颗fiber树的好处是,通过FiberRootNodecurrent指针的切换就可以实现上一次的workInProgress树,变为这一次的current树。而current树,由alternate联系的树,将会被当作workInProgress树,而且由于第二次更新建立了alternate联系,所以这一次每次createInProgress的时候不用再重新创建 workInProgress fiber节点了,可以通过复用来获得这一次新的的workInProgress fiber节点。

最后再去completework -> commit阶段, 最终完成第二次更新。