【React源码 - Fiber架构之Renderer】
作者:mmseoamin日期:2024-01-30

前言

本文主要将的是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
      function commitBeforeMutationEffects() {
        while (nextEffect !== null) {
          const current = nextEffect.alternate;
          if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
            // ...focus blur相关
          }
          const effectTag = nextEffect.effectTag;
          // 调用getSnapshotBeforeUpdate
          if ((effectTag & Snapshot) !== NoEffect) {
            commitBeforeMutationEffectOnFiber(current, nextEffect);
          }
          // 调度useEffect
          if ((effectTag & Passive) !== NoEffect) {
            if (!rootDoesHavePassiveEffects) {
              rootDoesHavePassiveEffects = true;
              scheduleCallback(NormalSchedulerPriority, () => {
                flushPassiveEffects();
                return null;
              });
            }
          }
          nextEffect = nextEffect.nextEffect;
        }
      }
      

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

      getSnapshotBeforeUpdate

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

      调度useEffect

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

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

      Mutation

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

      • 根据ContentReset effectTag重置文字节点
      • 更新ref
      • 根据effectTag(flags)分别处理,其中effectTag包括(Placement | Update | Deletion | Hydrating)
        function   commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
          // 遍历effectList
          while (nextEffect !== null) { 
            const effectTag = nextEffect.effectTag;
            // 根据 ContentReset effectTag重置文字节点
            if (effectTag & ContentReset) {
              commitResetTextContent(nextEffect);
            }
            // 更新ref
            if (effectTag & Ref) {
              const current = nextEffect.alternate;
              if (current !== null) {
                commitDetachRef(current);
              }
            }
            // 根据 effectTag 分别处理
            const primaryEffectTag = 
              effectTag & (Placement | Update | Deletion | Hydrating); 
            switch (primaryEffectTag) { 
              // 插入DOM
              case Placement: {
                commitPlacement (nextEffect);
                nextEffect.effectTag &= ~Placement;
                break;
              }
              // 插入DOM 并 更新DOM
              case PlacementAndUpdate: {
                // 插入
                commitPlacement(nextEffect);
                nextEffect.effectTag &= ~Placement;
                // 更新
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
              }
              // SSR
              case Hydrating: { 
                nextEffect.effectTag &= ~Hydrating;
                break;
              }
              // SSR
              case HydratingAndUpdate: {
                nextEffect.effectTag &= ~Hydrating;
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
              }
              // 更新DOM
              case Update: {
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
              }
              // 删除DOM
              case Deletion: {
                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分别处理。

          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,
                    );
                  }
                }
          

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

          function   updateDOMProperties(
            domElement: Element,
            updatePayload: Array,
            wasCustomComponentTag: boolean,
            isCustomComponentTag: boolean,
          ): void {
            // TODO: Handle wasCustomComponentTag
            for (let i = 0; i < updatePayload.length; i += 2) {
              const propKey = updatePayload[i];
              const propValue = updatePayload[i + 1];
              if (propKey === STYLE) {
                setValueForStyles(domElement, propValue);
              } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
                setInnerHTML(domElement, propValue);
              } else if (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的别名

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

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

               case ClassComponent: {
                    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

                function   commitAttachRef(finishedWork: Fiber) {
                  const ref = finishedWork.ref;
                  if (ref !== null) {
                    const instance = finishedWork.stateNode;
                    let instanceToUse;
                    switch (finishedWork.tag) {
                      case HostComponent:
                        instanceToUse = getPublicInstance(instance);
                        break;
                      default:
                        instanceToUse = instance;
                    }
                    // Moved outside to ensure DCE works with this flag
                    if (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技术揭秘