0


【React源码 - Fiber架构之Renderer】

前言

本文主要将的是Fiber架构三核心中渲染器Renderer,在Reconciler调度器中“归”过程回到rootfiber节点并执行完之后会调用commitroot并传入fiberRootNode来进入到Renderer阶段(commit阶段),在commit阶段会遍历effectList来进行DOM操作,在该阶段我将其细分为三个小阶段:

  • Before Mutation: DOM操作之前
  • Mutation: DOM操作
  • Layout: DOM操作之后

EffectList是一个单向链表,在Reconciler递归中执行completeWork时构建,最终形成一个以rootfiber.firstEffect为起点的单向链表,其他fiber.updateQueue以key,vakue的数组形式保存了需要更新的props

Before Mutation

这个阶段主要是遍历effectList并执行commitBeforeMutationEffects(旧版)/commitBeforeMutationEffectsImpl(新版), 主要做了一下三件事:

  • 处理DOM渲染/删除的autoFocus、blur逻辑
  • 调用getSnapshotBeforeUpdate生命周期钩子
  • 通过scheduleCallback调度useEffect
functioncommitBeforeMutationEffects(){while(nextEffect !==null){const current = nextEffect.alternate;if(!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !==null){// ...focus blur相关}const effectTag = nextEffect.effectTag;// 调用getSnapshotBeforeUpdateif((effectTag & Snapshot)!== NoEffect){commitBeforeMutationEffectOnFiber(current, nextEffect);}// 调度useEffectif((effectTag & Passive)!== NoEffect){if(!rootDoesHavePassiveEffects){
        rootDoesHavePassiveEffects =true;scheduleCallback(NormalSchedulerPriority,()=>{flushPassiveEffects();returnnull;});}}
    nextEffect = nextEffect.nextEffect;}}

commitBeforeMutationEffectOnFiber是commitBeforeMutationLifeCycles的别名,主要是生命周期的调用。

getSnapshotBeforeUpdate

从Reactv16开始,componentWillXXX钩子前增加了UNSAFE_前缀。原因是16用FIber架构重写,将原来的Stack Reconciler改为Fiber Reconciler后,render阶段可能会执行多次,而componentWillXXX钩子都存在于render阶段,这就会导致重复执行,设想如果我们在支付的时候,执行了多次,那就玩大了,所以进行了标记。为了解决这个问题,React提供了一个新的钩子getSnapshotBeforeUpdate,它可以保存DOM更新前的信息快照,然后给到componentDidUpdate. 并且它是在commit阶段执行的,不会重复执行。

调度useEffect

// 调度useEffectif((effectTag & Passive)!== NoEffect){if(!rootDoesHavePassiveEffects){
        rootDoesHavePassiveEffects =true;scheduleCallback(NormalSchedulerPriority,()=>{flushPassiveEffects();returnnull;});}}

代码中可见,scheduleCallback方法由Scheduler模块提供,用于以某个优先级异步调度一个回调函数。而flushPassiveEffects就是用来调度useEffect的。

Mutation

该阶段主要是来执行DOM的一些操作,主要也是遍历EffectList根据fiber的tag来处理不同的逻辑并更新对应的ref,主要做了以下事情:

  • 根据ContentReset effectTag重置文字节点
  • 更新ref
  • 根据effectTag(flags)分别处理,其中effectTag包括(Placement | Update | Deletion | Hydrating)
functioncommitMutationEffects(root: FiberRoot, renderPriorityLevel){// 遍历effectListwhile(nextEffect !==null){const effectTag = nextEffect.effectTag;// 根据 ContentReset effectTag重置文字节点if(effectTag & ContentReset){commitResetTextContent(nextEffect);}// 更新refif(effectTag & Ref){const current = nextEffect.alternate;if(current !==null){commitDetachRef(current);}}// 根据 effectTag 分别处理const primaryEffectTag = 
      effectTag &(Placement | Update | Deletion | Hydrating);switch(primaryEffectTag){// 插入DOMcasePlacement:{commitPlacement(nextEffect);
        nextEffect.effectTag &=~Placement;break;}// 插入DOM 并 更新DOMcasePlacementAndUpdate:{// 插入commitPlacement(nextEffect);

        nextEffect.effectTag &=~Placement;// 更新const current = nextEffect.alternate;commitWork(current, nextEffect);break;}// SSRcaseHydrating:{ 
        nextEffect.effectTag &=~Hydrating;break;}// SSRcaseHydratingAndUpdate:{
        nextEffect.effectTag &=~Hydrating;const current = nextEffect.alternate;commitWork(current, nextEffect);break;}// 更新DOMcaseUpdate:{const current = nextEffect.alternate;commitWork(current, nextEffect);break;}// 删除DOMcaseDeletion:{commitDeletion(root, nextEffect, renderPriorityLevel);break;`case HostComponent: {
      const instance: Instance = finishedWork.stateNode;
      if (instance != null) {
        // Commit the work prepared earlier.
        const newProps = finishedWork.memoizedProps;
        // For hydration we reuse the update path but we treat the oldProps
        // as the newProps. The updatePayload will contain the real change in
        // this case.
        const oldProps = current !== null ? current.memoizedProps : newProps;
        const type = finishedWork.type;
        // TODO: Type the updateQueue to be specific to host components.
        const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
        finishedWork.updateQueue = null;
        if (updatePayload !== null) {
          commitUpdate(
            instance,
            updatePayload,
            type,
            oldProps,
            newProps,
            finishedWork,
          );
        }
      }`}}

    nextEffect = nextEffect.nextEffect;}}

Placement Effect

当fiber.effectTag为Placement时,则通过commitPlacement函数,进行DOM的插入操作。

  • 获取父级DOM节点。其中finishedWork为传入的Fiber节点
  • 获取Fiber节点的DOM兄弟节点
  • 根据DOM兄弟节点是否存在决定调用parentNode.insertBefore或parentNode.appendChild执行DOM插入操作。

Update Effect

当Fiber节点含有Update effectTag,意味着该Fiber节点需要更新。调用的方法为commitWork,他会根据Fiber.tag分别处理。

caseHostComponent:{constinstance: Instance = finishedWork.stateNode;if(instance !=null){// Commit the work prepared earlier.const newProps = finishedWork.memoizedProps;// For hydration we reuse the update path but we treat the oldProps// as the newProps. The updatePayload will contain the real change in// this case.const oldProps = current !==null? current.memoizedProps : newProps;const type = finishedWork.type;// TODO: Type the updateQueue to be specific to host components.constupdatePayload:null| UpdatePayload =(finishedWork.updateQueue: any);
        finishedWork.updateQueue =null;if(updatePayload !==null){commitUpdate(
            instance,
            updatePayload,
            type,
            oldProps,
            newProps,
            finishedWork,);}}

当fiber.tag为HostComponent,会调用commitUpdate。最终会在updateDOMProperties 中将render阶段 completeWork 中为Fiber节点赋值的updateQueue对应的内容渲染在页面上。

functionupdateDOMProperties(domElement: Element,updatePayload: Array<any>,wasCustomComponentTag: boolean,isCustomComponentTag: boolean,):void{// TODO: Handle wasCustomComponentTagfor(let i =0; i < updatePayload.length; i +=2){const propKey = updatePayload[i];const propValue = updatePayload[i +1];if(propKey ===STYLE){setValueForStyles(domElement, propValue);}elseif(propKey ===DANGEROUSLY_SET_INNER_HTML){setInnerHTML(domElement, propValue);}elseif(propKey ===CHILDREN){setTextContent(domElement, propValue);}else{setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);}}}

Deletion effect

当Fiber节点含有Deletion effectTag,意味着该Fiber节点对应的DOM节点需要从页面中删除。调用的方法为commitDeletion。

  • 递归调用Fiber节点及其子孙Fiber节点中fiber.tag为ClassComponent的componentWillUnmount 生命周期钩子,从页面移除Fiber节点对应DOM节点
  • 解绑ref
  • 调度useEffect的销毁函数

Layout

该阶段之所以称为layout,因为该阶段的代码都是在DOM修改完成(mutation阶段完成)后执行的。该阶段触发的生命周期钩子和hook可以直接访问到已经改变后的DOM,即该阶段是可以参与DOM layout的阶段。

注意:由于 JS 的同步执行阻塞了主线程,所以此时 JS 已经可以获取到新的DOM,但是浏览器对新的DOM并没有完成渲染。

同before mutation、mutation阶段一样,都是遍历effectList来,并执行对应函数来进行不同的处理,这里调用的是commitLayoutEffects,主要做了两件事:

  • commitLayoutEffectOnFiber(调用生命周期钩子和hook相关操作)
  • commitAttachRef(赋值 ref)

commitLayoutEffectOnFiber

commitLayoutEffectOnFiber是commitLifeCycles的别名

functioncommitLayoutEffects(root: FiberRoot,committedLanes: Lanes){while(nextEffect !==null){const effectTag = nextEffect.effectTag;// 调用生命周期钩子和hookif(effectTag &(Update | Callback)){const current = nextEffect.alternate;commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);}// 赋值refif(effectTag & Ref){commitAttachRef(nextEffect);}

    nextEffect = nextEffect.nextEffect;}}

在commitLifeCycles函数中,当fiber.tag为ClassComponent时,会通过current === null?区分是mount还是update,调用componentDidMount 或componentDidUpdate 。

caseClassComponent:{const instance = finishedWork.stateNode;if(finishedWork.flags & Update){if(current ===null){if(
            enableProfilerTimer &&
            enableProfilerCommitHooks &&
            finishedWork.mode & ProfileMode
          ){try{startLayoutEffectTimer();
              instance.componentDidMount();}finally{recordLayoutEffectDuration(finishedWork);}}else{
            instance.componentDidMount();}}

以下函数也会在commitLifeCycles中调用:

  • 触发状态更新的this.setState如果赋值了第二个参数回调函数
  • 对于FunctionComponent及相关类型,他会调用useLayoutEffect hook的回调函数,调度useEffect的销毁与回调函数
  • 对于HostRoot,即rootFiber,如果赋值了第三个参数回调函数,也会在此时调用

commitAttachRef

commitAttachRef函数就比较简单,就是获取DOM实例,并更新ref

functioncommitAttachRef(finishedWork: Fiber){const ref = finishedWork.ref;if(ref !==null){const instance = finishedWork.stateNode;let instanceToUse;switch(finishedWork.tag){caseHostComponent:
        instanceToUse =getPublicInstance(instance);break;default:
        instanceToUse = instance;}// Moved outside to ensure DCE works with this flagif(enableScopeAPI && finishedWork.tag === ScopeComponent){
      instanceToUse = instance;}if(typeof ref ==='function'){ref(instanceToUse);}else{

      ref.current = instanceToUse;}}}

至此,commit阶段也完成了,这时候再将双缓存树的切换fiberRootNode的current指向current Fiber树,就可以更新试图。

Q&A

Q: useEffect和useLayoutEffect的区别?
A:useLayoutEffect从上一次更新的销毁函数调用(mutation阶段)到本次更新的回调函数调用(layout阶段)是同步执行的。
而useEffect则需要先在before mutation调度,在Layout阶段完成后再异步执行。

参考文档

React技术揭秘

标签: react.js 架构 前端

本文转载自: https://blog.csdn.net/weixin_44273311/article/details/135482292
版权归原作者 weixin_44273311 所有, 如有侵权,请联系我们删除。

“【React源码 - Fiber架构之Renderer】”的评论:

还没有评论