React Update-- 双缓存Fiber树
学习源码不易,和大家一起共勉!
前文: [React Origin Code] 2022年来聊聊React Mount-- renderRootSync当中,我们刚刚说完 React Mount,今天我们继续来聊聊React update,在聊update之前,我们还需要再次回到mount阶段,这次主要把握和关注两个概念,workInProgress树和 current树的双缓冲Fiber机制。这在React update阶段起到了至关重要的作用。
关于双缓存树的资料
下面我们以React 计数器案例来聊聊React update逻辑
图一:计数器案例来说明React update逻辑
createWorkInProgress
下面这段总结读完本文,可以回过头来看看:
createWorkInProgress函数:mount的时候只会走一次,就是创建workInProgress树的根节点,mount的其他节点不进到createWorkInProgress函数,因为此时没有currentfiber树,其他节点的current === null,当执行到reconcileChildren函数的时候,走的是mountChildFibers,只有更新的时候,current !== null ,走reconcileChildrenFibers的时候,才让每个子节点先去递归执行createWorkProgress,复用建立workInProgressfiber节点,最终建立好workInProgress树和 current树,并且用alternate属性,对两棵树的节点进行联系。
图二:不同的reconcilChildren逻辑
beginwork前的fiber树
关于mount阶段, beginwork函数的作用和流程,我们在前文已经详细聊过了, 在当前workInProgress fiber节点bedginwork之前,会调用createWorkInProgress函数。
mount的时候,在刚进入createWorkInProgreess的函数上打上断点,打印fiber树。结果是这样的。
图三:monunt 刚进入createWorkInProgreess的函数的打印结果
图四:mount 刚进入createWorkInProgreess的currentFiber树
mount的时候,执行的第一次也是最后一次的createWorkInProgreess函数结果是这样的
图五:mount的时候,执行的第一次也是最后一次的createWorkInProgreess后的Fiber树
然后从workProgreess树的根fiber节点开始 mount,关于mount的流程这篇文章很详细,mount之后结果是这样的。
图六:mount fiber树之后结果
细节注意:下一次更新之前,刚刚 mount时的workInProgress树会被变成current树,这里的变,也仅仅时需要改变FiberRootNode.current指针而已。这也是双缓存机制的魅力所在。
第一次触发计数器 + 1 -> update
mount结束后,我们触发计数器+1, 开始第一次更新,我们由上图可以知道,mount之后,第一次更新之前只有根节点有自己的alternate,所以在createWorkInProgress函数时,根节点fiber可以复用current树的 fiber,由于复用current fiber之后其child也被复用,所以结果是这样的。
第一次更新,除了根节点FiberRootNode之外其余节点curretn.alternatte都是null,所以每次到createWorkInProgress函数, 都会通过createFiber去创建新的workProgress节点,当前workInprogressfiber就会和 current fiber建立alternate的相互链接。然后去递归beginwork,reconcileChildren,然后再让children fiber去依次按照child -> sibling的顺序去createWorkInProgress。从而通过alternate属性完整建立起workInProgress树和 current树的联系。
mount和第一次更新还有一个区别,第一次更新reconcileChildren也进入了不一样的逻辑,这一次current fiber节点不再是null了,会进入reconcileChildrenFibers而不是 mountChildrenFibers这时shouldTraceEffect 会变为true给一些需要新插入的节点打上effectTag Placement, Delete…….
最终创建建立双缓存Fiber树的结果是这样的:
第一次更新时,beginWork还有一个优化逻辑,workInprogressfiber会和 current fiber 通过新旧props 对比,新旧type对比,以及是否有context的改变,如果没有都没有改变,则如果命中了这条优化逻辑,那么就会走bailoutOnAlreadyFinishWork逻辑,会执行cloneChildFibers返回子fiber。如果没有命中,继续根据不同的type来updatae不同类型的标签组件。
第一次更新时,beginWork还有一个优化逻辑 bailoutOnAlreadyFinishWork
completework
completework是beginWork深度优先遍历的回溯阶段,mount阶段的completework已经在前文聊过,这里我们不做赘述。我们这里主要来聊聊update阶段,update时会通过diffProperties对新旧props进行diff, 删除,增加或者来收集变化属性到workInProgress fiber节点的updateQueue数组当中,第i属性为变化的属性,i + 1属性是属性变化的值并且将需要更新属性的节点打上effectTag -> update,最后回溯的过程当中将所有打上effectTag的fiber节点收集到effectList链表当中。
最终当 complete 回溯到根节点的时候,从根节点出发的firstEffect属性 就指向effectList链表当中需要commit的fiber,当前fiber的next是下一个需要commit的节点,依次类推,一直到lastEffect结束。
最后形成的effectList链表是这样的
最终形成的effectList链表
最后`commitWork` 根节点。进入`commit`阶段更新渲染视图。建立effectList链表的好处是不用再像renderer阶段一样,每次无论是mount,还是更新都要从FiberRootNode根节点开始深度优先遍历。有了effectList链表,在commit的阶段,就只对需要更新的fiber进行commit来更新渲染视图。不用去深度优先遍历,回溯的时候去appendChild
第二次触发计数器 + 1 -> update
第二次更新前的Fiber树
第二次更新很关键,因为这时内存当中存在两颗react fiber树, 有了这两颗fiber树的好处是,通过FiberRootNode的current指针的切换就可以实现上一次的workInProgress树,变为这一次的current树。而current树,由alternate联系的树,将会被当作workInProgress树,而且由于第二次更新建立了alternate联系,所以这一次每次createInProgress的时候不用再重新创建 workInProgress fiber节点了,可以通过复用来获得这一次新的的workInProgress fiber节点。
最后再去completework -> commit阶段, 最终完成第二次更新。