likes
comments
collection
share

React18.2x源码解析(四)commit阶段【DOM渲染与回调处理】

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

在本系列的第二,第三章节已经完整的讲述了render阶段的scheduler调度流程和reconciler协调流程。

本节将深入理解react应用渲染流程的最后一个阶段commit阶段。

commitRootImpl

接着第二章节结束的内容,继续查看commitRootImpl方法:

// packages\react-reconciler\src\ReactFiberWorkLoop.new.js

function commitRootImpl() {
    ...
}

这里的commitRootImpl方法就是commit阶段的全部内容,但是commitRootImpl方法里面内容非常多,这里我们将commitRootImpl方法的内容按执行顺序分成两个部分:

  • 准备工作:commit阶段的准备工作。
  • 主要工作:commit阶段的主要工作。

其中主要工作又可以分成三个子阶段,它是commit的核心逻辑,后面将会详细讲解。

准备工作

// packages\react-reconciler\src\ReactFiberWorkLoop.new.js

function commitRootImpl() {
  do {
    // 循环处理副作用Effects,直到处理完成
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

  // 取出要提交的工作: 代表workInProgress HostRootFiber 即render阶段构建的workInProgress Tree的HostRootFiber
  const finishedWork = root.finishedWork;
  // 取出Lanes
  const lanes = root.finishedLanes;

  // 重置root状态
  root.finishedWork = null;
  root.finishedLanes = NoLanes;

  // 重置回调任务与优先级
  root.callbackNode = null;
  root.callbackPriority = NoLane;

  // 合并同一批次的lanes,通过位运算,产生新的lane
  let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);

  const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
  remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);

  // 标记root完成
  markRootFinished(root, remainingLanes);
    
  // 如果存在挂起的副作用,使用调度器生成一个task任务来处理它们
  if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      pendingPassiveEffectsRemainingLanes = remainingLanes;

      // Passive表示FC组件中定义了useEffect且需要触发的回调函数
      pendingPassiveTransitions = transitions;

      // 调度一个宏任务,处理函数组件的effects【只是调度了一个异步任务,effect回调实际上还没有处理】
      scheduleCallback(NormalSchedulerPriority, () => {
        // 这个回调就是处理useEffect的
        flushPassiveEffects();
      });
    }
  }
          
  ...
}

首先一进入commitRootImpl方法,就是一个do while循环,这个循环体调用了一个flushPassiveEffects方法,Passive标记对应的是useEffect的副作用操作,所以这个方法的作用就是冲刷函数组件的useEffect的副作用钩子。

接着我们看下面又有一段调用flushPassiveEffects方法的代码:

// finishedWork即是hostFiber
if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||(finishedWork.flags & PassiveMask) !== NoFlags) {
    // ... 处理副作用
}

这里的判断条件有两个:

  • 判断HostFiber节点自身是否存在与PassiveMask相关的副作用。
  • 判断HostFiber的子节点是否存在与PassiveMask相关的副作用。

只要满足任何一个条件,都需要进入内部处理副作用,接着再看具体的处理方法:

scheduleCallback(NormalSchedulerPriority, 
// 这个回调就是处理useEffect的
  () => {
    // 处理副作用
    flushPassiveEffects();
});

依然是使用scheduleCallback方法【这个方法的调度逻辑在第二章已经讲述】创建一个新的任务task,最后会生成一个新的宏任务来异步处理副作用,这里处理副作用的方法就是flushPassiveEffects函数。

关于flushPassiveEffects方法的具体内容会在useEffect章节中来专门讲解。

关于后面的这个flushPassiveEffects方法其实很好理解,就是专门用于页面渲染完成后来执行useEffect的回调钩子,因为commit阶段的渲染是同步执行,所以通过scheduleCallback方法调度后,就能保证flushPassiveEffects方法一定是在本次DOM渲染完成之后执行。

但也正是因为调度的原因,还得保证本次commit阶段调度的useEffect必须得在下一次commit之前执行,所以才会在每一次commit之前先执行一次flushPassiveEffects,清理掉可能存在的上一次的useEffect

接着从root应用根节点上取出本次更新创建完成的FiberTree和更新优先级:

const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;

然后立即重置root节点上的一些状态,方便下次更新任务的内容挂载:

// 重置虚拟DOM树和优先级
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 重置回调任务与优先级
root.callbackNode = null;
root.callbackPriority = NoLane;

总结: commit阶段的准备工作主要就是:清理可能存在的上一次的useEffect,然后调度本次commit阶段的useEffect,最后重置一些基本信息方便下个更新任务的内容挂载。

主要工作

下面我们继续查看主要工作的内容,它是commit阶段的核心逻辑,这部分内容可以分为三个子阶段:

// packages\react-reconciler\src\ReactFiberWorkLoop.new.js

function commitRootImpl() {
  ...
    
  // 检查HostFiber的子孙元素是存在副作用
  const subtreeHasEffects = (finishedWork.subtreeFlags 
                             &(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;
  // 检查HostFiber自身存在副作用
  const rootHasEffect = (finishedWork.flags &(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
    NoFlags;
    
  // 任何一个有副作用,说明需要更新,进入commit逻辑
  if (subtreeHasEffects || rootHasEffect) {

    # 进入三个子阶段
    const prevTransition = ReactCurrentBatchConfig.transition;
    ReactCurrentBatchConfig.transition = null;
    // 获取当前更新优先级
    const previousPriority = getCurrentUpdatePriority();
    // 设置同步优先级,commit必须同步执行
    setCurrentUpdatePriority(DiscreteEventPriority);
    // 当前上下文
    const prevExecutionContext = executionContext;
    executionContext |= CommitContext;

    // 在调用生命周期之前将其重置为null
    ReactCurrentOwner.current = null;
      
    // 1,BeforeMutation 【这个阶段执行与flags相关的副作用】
    const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
      root,
      finishedWork,
    );

    // 2,完成真实的dom渲染,更新页面【这个阶段主要是对DOM的增删改】
    commitMutationEffects(root, finishedWork, lanes);

    // 渲染后,重置container容器信息
    resetAfterCommit(root.containerInfo);

    // 根据Fiber双缓冲机制,完成Current Fiber Tree的切换
    // 其实也并不一定叫切换,就是将最新work内容存储为当前的内容,下一次的work就可以利用当前的内容
    // 注意:到这里是页面已经完成了更新渲染,这个交互Fiber Tree是为了保留最新的Tree,提供给下次更新使用
    // 同时也方便调用生命周期钩子时是最新的DOM
    root.current = finishedWork;

    // 3, Layout阶段【这个阶段会执行一些副作用:比如class组件的mount/update钩子,Current Fiber Tree已经是本次更新的Fiber Tree】
    // Layout名称来源于useLayoutEffect,函数组件的useLayoutEffect callback会这个阶段执行
    commitLayoutEffects(finishedWork, root, lanes);

  } else {
    // No effects.
    // 本次更新没有副作用, 直接覆盖
    root.current = finishedWork;
  }
  
  ...
}

首先定义了两个状态变量:

  • subtreeHasEffects:检查HostFiber的子孙元素是否存在副作用。
  • rootHasEffect:检查HostFiber自身是否存在副作用。

并且这两个变量的判断条件都跟四个mask掩码相关。

BeforeMutationMask | MutationMask | LayoutMask | PassiveMask

只要满足其中任何一个mask,则变量结果为true。表示存在相关的副作用,需要执行commit逻辑。

在这里我们首先要了解react源码中一些常规flags标记对应的场景:

// packages\react-reconciler\src\ReactFiberFlags.js

Placement // 代表当前Fiber或者子孙Fiber存在需要插入或者移动的dom元素或者文本【hostComponent,hostText】
Update // 1.触发class组件的mount/update生命周期钩子函数; 2.hostComponent发生属性变化; 3.hostText发生文本变化; 4.Fun组件定义了useLayoutEffect
Deletion // 当前节点存在删除操作
ChildDeletion // 子节点存在需要删除的dom或者文本【hostComponent,hostText】
ContentReset // 清空hostComponent,即DOM节点的文本内容
Callback // 调用this.setState时,传递了回调函数参数
Ref // ref引用的创建与更新
Snapshot // 触发class组件的getSnapshotBeforeUpdate方法【使用较少】
Passive // 触发函数组件的useEffect钩子

知道了这些flags基本的定义,我们再看这四个mask的定义:

BeforeMutationMask = Update | Snapshot;
MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility;
LayoutMask = Update | Callback | Ref | Visibility;
PassiveMask = Passive | ChildDeletion;

这四个mask包括了上面的各种flags,而react应用的加载和更新基本都会存在各种副作用,所以默认都会进入这个判断,然后开始三个commit子阶段的逻辑执行,下面我们就按执行顺序逐个解析每个阶段执行逻辑。

在讲解之前还有一个处理要注意:

// 设置为同步优先级
setCurrentUpdatePriority(DiscreteEventPriority);

设置当前更新的优先级为DiscreteEventPriority【同步优先级】,代表commit阶段的任务是立即执行的同步任务,并且是不可中断的。这在下面的三个子阶段里面都可以发现这个特点:依然采用递归遍历来处理所有Fiber节点的副作用,但是循环会持续到工作完成,没有像reconciler协调流程有可以中断的条件和机会。

1,commitBeforeMutationEffects

commitBeforeMutationEffects(root, finishedWork);

前面我们已经知道了BeforeMutationMask的定义:

BeforeMutationMask = Update | Snapshot;

BeforeMutationMask就是表示BeforeMutation阶段所需要执行的哪些副作用类型:

  • 触发class组件的getSnapshotBeforeUpdate钩子。
  • hostComponent发生属性变化,需要更新执行。
  • hostText发生文本变化,需要更新执行。

查看commitBeforeMutationEffects方法:

// packages\react-reconciler\src\ReactFiberCommitWork.new.js

export function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
) {
    
  focusedInstanceHandle = prepareForCommit(root.containerInfo);
  // 设置第一个处理的节点为 hostFiber
  nextEffect = firstChild;
  # 提交开始
  commitBeforeMutationEffects_begin();

  const shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;
  return shouldFire;
}

初始化nextEffect变量,这里的firstChild代表第一个处理的节点为hostFiber

然后调用commitBeforeMutationEffects_begin,开始递归遍历FiberTree,处理有副作用的Fiber节点。

commitBeforeMutationEffects_begin

查看commitBeforeMutationEffects_begin方法:

// packages\react-reconciler\src\ReactFiberCommitWork.new.js

function commitBeforeMutationEffects_begin() {
  # while循环,处理所有节点
  while (nextEffect !== null) {
    const fiber = nextEffect;
    // 取出子节点 刚开始是hostFiber节点的child: 即App 组件
    const child = fiber.child;
    // 如果该fiber的子节点存在BeforeMutation阶段相关的flgas标记 且 child不为null;  则继续循环,
    if ((fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && child !== null) {
      child.return = fiber;
      // 设置child为下一个处理的节点
      nextEffect = child;
    } else {
      // 只有当fiber的子节点不存在BeforeMutation阶段相关的flgas标记 且 child为null; 【叶子节点】
      // 和reconciler流程一样,第一个进入complete工作的都是【div react源码调试】节点
      commitBeforeMutationEffects_complete();
    }
  }
}

在这里我们可以发现,commitBeforeMutationEffects_begin方法的结构和reconciler流程中的beginWork工作逻辑非常相似,唯一的区别就是BeforeMutation阶段这里的循环并没有中断的条件,必须将所有Fiber节点都循环处理一遍,才会跳出循环,代表工作结束。

继续查看commitBeforeMutationEffects_begin方法,每次循环都会将它的child子节点取出,判断是否满足以下条件:

  • fiber的子节点存在BeforeMutation阶段相关的flags标记 。
  • child不为null

如果同时满足这两个条件,则将child设置为新的nextEffect,开启下一个循环。如果不满足这两个条件,则说明当前节点就是需要处理副作用的节点,即需要进入commitBeforeMutationEffects_complete方法,执行它的complete工作。

为什么满足条件要继续循环,不满足条件才进入complete。因为满足条件代表它的子节点有需要处理的内容,而不是本节点,所以才需要进行下一轮的循环,不满足条件时则代表本节点有需要执行的副作用。

commitBeforeMutationEffects_complete

查看commitBeforeMutationEffects_complete方法:

// packages\react-reconciler\src\ReactFiberCommitWork.new.js

function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    try {
      // 处理当前节点的副作用
      commitBeforeMutationEffectsOnFiber(fiber);
    } catch (error) {
      captureCommitPhaseError(fiber, fiber.return, error);
    }
    // 取出兄弟节点,
    const sibling = fiber.sibling;
    if (sibling !== null) {
      sibling.return = fiber.return;
      // 将兄弟节点设置为下一个nextEffect 【回到begin工作】
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }
}

commitBeforeMutationEffects_complete方法也是一个while循环,对当前Fiber节点执行flags标记对应的操作,即执行commitBeforeMutationEffectsOnFiber方法。执行完成后,如果当前Fiber节点存在sibling兄弟节点,则将兄弟节点设置为最新的nextEffect,退出当前函数,开启兄弟节点的begin工作。如果不存在兄弟节点,则将当前Fiber节点的父级节点设置为最新的nextEffect,执行父级节点的commitBeforeMutationEffects_complete工作。

根据上面可以看出,BeforeMutation阶段逻辑和之前创建FiberTree的逻辑基本相同,都是深度优先遍历的顺序从HostFiber根节点开始【自上而下】遍历处理每一个满足条件的Fiber节点,执行flags对应的副作用操作,即相似的begincomplete工作逻辑。这里主要的执行内容在commitBeforeMutationEffectsOnFiber方法之中。

注意,其实commit阶段中三个子阶段的逻辑:基本都是以这种逻辑方式来处理相关的副作用内容。

commitBeforeMutationEffectsOnFiber

继续查看commitBeforeMutationEffectsOnFiber方法:

// packages\react-reconciler\src\ReactFiberCommitWork.new.js

function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate;
  // 副作用标记
  const flags = finishedWork.flags;

  // 判断满足flags的节点
  if ((flags & Snapshot) !== NoFlags) {
    setCurrentDebugFiberInDEV(finishedWork);

    // 根据节点的类型,进行不同的副作用处理
    switch (finishedWork.tag) {

      case ClassComponent: {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
 
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
        break;
      }
      // hostFiber节点的处理
      case HostRoot: {
        if (supportsMutation) {
          // 应用根节点
          const root = finishedWork.stateNode;
          // 设置textContent = ''; 也就是清空#div容器内容, 方便Mutation阶段的渲染
          clearContainer(root.containerInfo);
        }
        break;
      }
    }
  }
}

进入commitBeforeMutationEffectsOnFiber方法后,发现只有Snapshot标记的副作用才会执行。

BeforeMutationMask = Update | Snapshot;

也就是说BeforeMutationMask虽然包含了两种标记的副作用,但实际只会执行Snapshot标记的副作用。

BeforeMutation阶段总结

所以BeforeMutation阶段最终只会执行这两种副作用:

  • 触发类组件的getSnapshotBeforeUpdate钩子。
  • 处理HostRoot类型节点【HostFiber】,清空#root容器内容, 方便下面Mutation阶段的DOM挂载。

2,commitMutationEffects

commitMutationEffects(root, finishedWork, lanes);

前面我们已经知道了MutationMask的定义:

MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility;

MutationMask就是代表Mutation阶段所需要执行的哪些副作用类型,虽然可以看到MutationMask集成了很多的副作用标记,但是Mutation阶段的重点内容:还是针对Fiber节点上DOM内容的处理,然后将最终的DOM内容渲染到页面。

查看commitMutationEffects方法:

// packages\react-reconciler\src\ReactFiberCommitWork.new.js

export function commitMutationEffects(
  root: FiberRoot,
  finishedWork: Fiber, // hostFiber
  committedLanes: Lanes,
) {
  inProgressLanes = committedLanes;
  inProgressRoot = root;

  // 从hostFiber节点开始处理
  commitMutationEffectsOnFiber(finishedWork, root, committedLanes);

  inProgressLanes = null;
  inProgressRoot = null;
}

直接查看commitMutationEffectsOnFiber方法,开启Mutation阶段的逻辑处理。

commitMutationEffectsOnFiber

查看commitMutationEffectsOnFiber方法:

// packages\react-reconciler\src\ReactFiberCommitWork.new.js

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {

  // 旧hostFiber 节点
  const current = finishedWork.alternate;
  // 取出dom操作的标识flags
  const flags = finishedWork.flags;

  // 根据节点tag,执行不同的逻辑
  switch (finishedWork.tag) {

    // 函数组件处理
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      // recursivelyTraverse:递归遍历
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      // app组件跳出了循环,向下继续执行
      commitReconciliationEffects(finishedWork);

      if (flags & Update) {
        try {
          commitHookEffectListUnmount(
            HookInsertion | HookHasEffect,
            finishedWork,
            finishedWork.return,
          );
          commitHookEffectListMount(
            HookInsertion | HookHasEffect,
            finishedWork,
          );
        } catch (error) {
          captureCommitPhaseError(finishedWork, finishedWork.return, error);
        }
        if (
          enableProfilerTimer &&
          enableProfilerCommitHooks &&
          finishedWork.mode & ProfileMode
        ) {
          try {
            startLayoutEffectTimer();
            commitHookEffectListUnmount(
              HookLayout | HookHasEffect,
              finishedWork,
              finishedWork.return,
            );
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
          recordLayoutEffectDuration(finishedWork);
        } else {
          try {
            commitHookEffectListUnmount(
              HookLayout | HookHasEffect,
              finishedWork,
              finishedWork.return,
            );
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }

    // 类组件处理
    case ClassComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);

      if (flags & Ref) {
        if (current !== null) {
          safelyDetachRef(current, current.return);
        }
      }
      return;
    }

    // 原生dom元素处理
    case HostComponent: {
      // 递归遍历
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      // 处理dom
      commitReconciliationEffects(finishedWork);

      if (flags & Ref) {
        if (current !== null) {
          safelyDetachRef(current, current.return);
        }
      }
      if (supportsMutation) {
        // 如果flags标记为ContentReset
        if (finishedWork.flags & ContentReset) {
          // dom实例
          const instance: Instance = finishedWork.stateNode;
          try {
            // 重置dom的内容
            resetTextContent(instance);
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }

        // 如果flgas标记为Update更新
        if (flags & Update) {
          // 取出当前节点对应的DOM对象
          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) {
              try {
                commitUpdate(
                  instance,
                  updatePayload,
                  type,
                  oldProps,
                  newProps,
                  finishedWork,
                );
              } catch (error) {
                captureCommitPhaseError(
                  finishedWork,
                  finishedWork.return,
                  error,
                );
              }
            }
          }
        }
      }
      return;
    }

    // 文本处理
    case HostText: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);

      if (flags & Update) {
        if (supportsMutation) {
          if (finishedWork.stateNode === null) {
            throw new Error(
              'This should have a text node initialized. This error is likely ' +
                'caused by a bug in React. Please file an issue.',
            );
          }

          const textInstance: TextInstance = finishedWork.stateNode;
          const newText: string = 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 oldText: string =
            current !== null ? current.memoizedProps : newText;

          try {
            commitTextUpdate(textInstance, oldText, newText);
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }

    // hostFiber根节点处理 【第一次会走根节点, 从根节点向下递归渲染dom】
    case HostRoot: {
      // recursivelyTraverse:递归遍历 【页面显示】
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);

      commitReconciliationEffects(finishedWork);

      if (flags & Update) {
        if (supportsMutation && supportsHydration) {
          if (current !== null) {
            const prevRootState: RootState = current.memoizedState;
            if (prevRootState.isDehydrated) {
              try {
                commitHydratedContainer(root.containerInfo);
              } catch (error) {
                captureCommitPhaseError(
                  finishedWork,
                  finishedWork.return,
                  error,
                );
              }
            }
          }
        }
        if (supportsPersistence) {
          const containerInfo = root.containerInfo;
          const pendingChildren = root.pendingChildren;
          try {
            replaceContainerChildren(containerInfo, pendingChildren);
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }
    
    // ...

    default: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);

      return;
    }
  }
}

commitMutationEffectsOnFiber方法内容很多,但是它的核心逻辑依然是switch case结构:根据当前Fiber节点tag值,对不同类型节点进行不同的逻辑处理。

要处理的组件类型有很多,我们主要掌握几个常见的组件类型处理逻辑:

  • FunctionComponent:函数组件。
  • ClassComponent:类组件。
  • HostComponent:DOM节点。
  • HostText:文本节点。
  • HostRootHostFiber根节点。

从上面几个组件类型的处理逻辑来看,它们都是一套相同的处理逻辑:

// 1,删除DOM
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// 2,插入DOM
commitReconciliationEffects(finishedWork);
// 3,更新(DOM内容)
Mutation阶段执行顺序

在讲具体的执行逻辑之前,我们先得明白Mutation阶段的执行顺序,这对我们理解DOM是如何渲染到页面的非常重要。

这里我们将上面的处理逻辑进行简化函数名【映射】,方便我们更好的理解:

commitMutationEffectsOnFiber  => commit()
recursivelyTraverseMutationEffects  这个方法中使用了循环,并且调用了上面的方法=> for()

// 2,插入DOM
commitReconciliationEffects(finishedWork);  // 2,3两部分视为一个内容 => content()
// 3,更新(DOM内容)

所以Mutation阶段的执行顺序就可以表示为:

Fiber顺序: HostFiber  =>    fun App    => 	 div.App  =>   ...
// 代码执行顺序
commit 
	        for
			      commit 
			  		  	    for
						  		   commit
						  				      for
						  				  		     commit
						  				  		  		      ...
						  				        content
						    content
	      content

执行顺序为从上往下,通过这种递归遍历方式,可以发现HostFiber虽然是第一个进入commit逻辑的节点,但是它的content内容却是最后一个执行,也就是说在HostFibercontent内容处理完成之后,即代表完整的DOM树已经渲染到页面。

实际上最后执行插入到页面的操作是在【fun App】组件节点上执行的,后续会有说明。这部分内容在第三章也有解释。

Mutation执行顺序图:

React18.2x源码解析(四)commit阶段【DOM渲染与回调处理】

recursivelyTraverseMutationEffects

首先查看recursivelyTraverseMutationEffects方法:

// packages\react-reconciler\src\ReactFiberCommitWork.new.js

function recursivelyTraverseMutationEffects(
  root: FiberRoot, // root
  parentFiber: Fiber, // HostFiber
  lanes: Lanes,
) {
  // 删除标记,是否存在
  const deletions = parentFiber.deletions;
  if (deletions !== null) {
    for (let i = 0; i < deletions.length; i++) {
      const childToDelete = deletions[i];
      try {
        // 执行删除副作用
        commitDeletionEffects(root, parentFiber, childToDelete);
      } catch (error) {
        captureCommitPhaseError(childToDelete, parentFiber, error);
      }
    }
  }

  // 如果子节点树有副作用标记
  if (parentFiber.subtreeFlags & MutationMask) {
    // 取出子节点
    let child = parentFiber.child;
    while (child !== null) {
      // 开始递归渲染
      commitMutationEffectsOnFiber(child, root, lanes);
      child = child.sibling;
    }
  }
}

根据上面的代码可以看出,recursivelyTraverseMutationEffects方法主要就两个逻辑:

  • 判断当前Fiber节点是否存在deletions删除标记,如果存在则循环deletions,删除子节点对应DOM元素的内容。
  • 遍历子树,递归调用commitMutationEffectsOnFiber

这里主要讲解第一点内容删除的逻辑,第二点遍历的内容就是前面Mutation阶段执行顺序的内容。

具体的删除逻辑就是执行commitDeletionEffects方法,真实的删除逻辑比较复杂,删除一个DOM元素要考虑很多东西,这里我们不会展开commitDeletionEffects方法,但还是要了解一下可能会造成的一些副作用影响:

  • 执行子树所有组件的unmount卸载逻辑。
  • 执行子树某些类组件的componentWillUnmount方法。
  • 执行子树某些函数组件的useEffectuseLayoutEffect等hooks的destory销毁方法。
  • 执行子树所有ref属性的卸载操作。

这里将删除DOM的逻辑放在每个组件的第一个执行,也是非常必要的。因为Mutation阶段的核心就是DOM操作,如果对应的DOM都已经被删除了,那么也就没有必要再去执行剩下的修改更新了。

commitReconciliationEffects

接着查看commitReconciliationEffects方法:

// packages\react-reconciler\src\ReactFiberCommitWork.new.js

function commitReconciliationEffects(finishedWork: Fiber) {
  const flags = finishedWork.flags;
  // 如果是插入/移动标记
  if (flags & Placement) {
    try {
      // 执行dom插入添加操作
      commitPlacement(finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
    finishedWork.flags &= ~Placement;
  }
  if (flags & Hydrating) {
    finishedWork.flags &= ~Hydrating;
  }
}

commitReconciliationEffects方法就是执行DOM的插入或者移动操作,判断当前Fiber节点是否存在Placement标记,存在就会执行commitPlacement方法,执行相关的DOM的操作。

commitPlacement

继续查看commitPlacement方法:

// packages\react-reconciler\src\ReactFiberCommitWork.new.js

function commitPlacement(finishedWork: Fiber): void {
  if (!supportsMutation) {
    return;
  }

  // finishedWork: fun App 它的父级节点为HostFIber 
  const parentFiber = getHostParentFiber(finishedWork);

  switch (parentFiber.tag) {
    # 普通DOM节点
    case HostComponent: {
      const parent: Instance = parentFiber.stateNode;
      if (parentFiber.flags & ContentReset) {
        resetTextContent(parent);
        parentFiber.flags &= ~ContentReset;
      }

      const before = getHostSibling(finishedWork);
      // 插入节点
      insertOrAppendPlacementNode(finishedWork, before, parent);
      break;
    }

    # 处理HostFiber节点的插入动作:
    case HostRoot:
    case HostPortal: {
      // #div
      const parent: Container = parentFiber.stateNode.containerInfo;
      // 无兄弟节点
      const before = getHostSibling(finishedWork);
      # 将离屏的DOM树插入到#div; 马上页面上显示出DOM内容
      insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
      break;
    }
  }
}

能够执行Placement副作用的,只有两种组件节点HostComponentHostRoot

对于HostComponent来说,就是常规的DOM添加和插入,即调用原生的DOM方法执行对应的逻辑:

// 原生DOM操作
parentNode.appendChild()
parentNode.insertBefore()
首屏渲染

注意:mount阶段,也就是react应用的首次加载时,只会执行一次Placement插入操作,这个执行对象是【App根组件】对应的Fiber节点。这个是reconciler协调流程中设置的,可以查看第三章关于此部分的逻辑,我们再来回顾一下:

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 在Mount阶段,只有hostFiber进入这个逻辑
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

在react应用的首次加载时【mount阶段】,只有HostFiber节点进入了else分支【else分支默认对应的是update阶段】,因为HostFiber节点所对应current始终是存在的。

export const reconcileChildFibers = ChildReconciler(true);

这里ChildReconciler调用时传入了true的状态,这个参数就是下面的shouldTrackSideEffects

所以HostFiber在创建它的child:也就是App根组件所对应的节点时【newFiber】,就被打上了Placement插入标记。

// place插入操作
function placeSingleChild(newFiber) {
  //  true && 新建的Fiber节点alternate都是null  newFiber是App根组件的fiber
  if (shouldTrackSideEffects && newFiber.alternate === null) {
    newFiber.flags |= Placement;
  }
  return newFiber;
}

mount阶段只有App根组件对应的Fiber节点存在插入标记,这样的好处是:在react应用的首次加载时【首屏渲染】,无需执行大量的插入操作,只需执行一次插入操作,即可将已经构建完成的离屏DOM树加载到页面。

这里我们可以通过源码调试来验证:

React18.2x源码解析(四)commit阶段【DOM渲染与回调处理】

此时对应的控制台输出:

React18.2x源码解析(四)commit阶段【DOM渲染与回调处理】

这里输出的Placement标记对应的Fiber节点就是:App根组件节点。因为commitPlacement方法中的判断是取的父级的tag值,App根组件的父级节点就是HostFiber,就会进入hostRoot分支逻辑:

function commitPlacement(finishedWork: Fiber): void {
  // finishedWork: fun App 它的父级节点为HostFIber 
  const parentFiber = getHostParentFiber(finishedWork);

  switch (parentFiber.tag) {
    ...
    
    // 处理HostFiber节点的插入动作:
    case HostRoot:
    case HostPortal: {
      // #root
      const parent = parentFiber.stateNode.containerInfo;
      // 无兄弟节点
      const before = getHostSibling(finishedWork);
      # 将离屏的DOM树插入到#root; 页面上立即就显示出DOM内容
      insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
      break;
    }
  }
}

这里的parent即是#root容器节点,App根组件没有兄弟节点,beforenull

进入insertOrAppendPlacementNodeIntoContainer方法:

function insertOrAppendPlacementNodeIntoContainer(
  node: Fiber,
  before: ?Instance,
  parent: Container,
): void {
  const {tag} = node; // App根组件节点
  // Host 或者 text
  const isHost = tag === HostComponent || tag === HostText;
  if (isHost) {
    const stateNode = node.stateNode;
    if (before) {
      insertInContainerBefore(parent, stateNode, before);
    } else {
      appendChildToContainer(parent, stateNode);
    }
  } else if (tag === HostPortal) {
    ...
  } else {
    # fun App节点的child ,也就是div.App 这个节点的内容就是一个离屏的DOM
    const child = node.child;
    if (child !== null) {
      # 将整个离屏的DOM树添加到 #div中,页面渲染完成
      insertOrAppendPlacementNodeIntoContainer(child, before, parent);
    }
  }
}

在进入insertOrAppendPlacementNodeIntoContainer方法后,又是根据节点类型进行不同处理,App根组件节点不满足上面的类型,只能进入else分支。然后取出App根组件节点的child,也就是【div.app】所对应的Fiber节点。

<div className="App">
    ...
</div>

因为函数组件节点是不会存储DOM结构的,只能向下寻找。App组件是根组件,它的根DOM就是当前DOM树的根DOM元素

React18.2x源码解析(四)commit阶段【DOM渲染与回调处理】

此时它的DOM内容就是已经构建完成的离屏DOM树:

React18.2x源码解析(四)commit阶段【DOM渲染与回调处理】

可以看见此时的页面还是空白内容,但当我们一执行insertOrAppendPlacementNodeIntoContainer方法:

// 相当于执行
#root .appendChild(div.App)

这棵完整的DOM树就会被立即渲染到页面上:

React18.2x源码解析(四)commit阶段【DOM渲染与回调处理】

Mutation阶段的核心逻辑就相当于基本执行完成了。

解析完了Mutation阶段整体的加载流程,下面我们再看每个组件节点一些不同的处理:

  • 函数组件:处理Effect hook相关的副作用。
// 循环effect链表执行effect.destory方法
commitHookEffectListUnmount();
// 循环effect链表执行effect.create方法
commitHookEffectListMount();

关于Effect hook的细节处理会放到useEffect章节中展开讲解。

  • 类组件:更新ref引用。
if (flags & Ref) {
  if (current !== null) {
     // 更新ref
	safelyDetachRef(current, current.return);
  }
}
  • HostComponent:关于DOM节点的逻辑处理最多。更新ref引用和重置dom内容,最后更新dom的属性和样式。
// 1,更新ref
if (flags & Ref) {
  if (current !== null) {
	safelyDetachRef(current, current.return);
  }
}
// 2,重置dom内容
if (finishedWork.flags & ContentReset) {
    // dom实例
    const instance: Instance = finishedWork.stateNode;
    // 重置dom内容
    resetTextContent(instance);
}
// 3,更新dom属性和样式
if (flags & Update) {
    const instance: Instance = finishedWork.stateNode;
    if (instance !== null) {
      const newProps = finishedWork.memoizedProps;
      const oldProps = current !== null ? current.memoizedProps : newProps;
      const type = finishedWork.type;
      const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
      commitUpdate(instance, updatePayload, type, oldProps, newProps, finishedWork);
    }
}
  • HostText:更新文本内容。
if (flags & Update) {
  const textInstance: TextInstance = finishedWork.stateNode;
  const newText: string = finishedWork.memoizedProps;
  const oldText: string = current !== null ? current.memoizedProps : newText;
  // 更新文本内容
  commitTextUpdate(textInstance, oldText, newText);
}
Mutation阶段总结

Mutation阶段的主要工作是对DOM内容的增删改操作,最后将构建完成的离屏DOM树渲染到页面。

3,commitLayoutEffects

commitLayoutEffects(finishedWork, root, lanes);

前面我们已经知道了LayoutMask的定义:

LayoutMask = Update | Callback | Ref | Visibility;

LayoutMask就是代表Layout阶段所需要执行的哪些副作用类型:

  • 类组件的componentDidMount/componentDidUpdate生命周期钩子函数的执行。
  • 类组件调用this.setState时传递的callback回调函数的会被保存到Fiber节点的updateQueue属性中在这里执行。
  • 执行函数组件的useLayoutEffect hook回调。

可以说Layout阶段的主要内容:就是在DOM渲染完成后,执行函数组件和类组件定义的一些callback回调函数。

查看commitLayoutEffects方法:

// packages\react-reconciler\src\ReactFiberCommitWork.new.js

export function commitLayoutEffects(
  finishedWork: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
): void {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  // HostFiber
  nextEffect = finishedWork;
  # 又是begin 和complete工作逻辑
  # 相当于递归循环触发每个组件的生命周期钩子函数 以及相关的hooks回调
  commitLayoutEffects_begin(finishedWork, root, committedLanes);

  inProgressLanes = null;
  inProgressRoot = null;
}

可以看出Layout阶段的处理逻辑还是begincomplete工作,继续以深度优先遍历的顺序,根据不同的组件类型,进行不同的逻辑处理,这里就不重复描述了,下面就直接贴出相关的代码。

在这里我们也可以看出,react源码中创建FiberTreereconciler协调流程和commit阶段的渲染流程都是基本相同的工作逻辑:

  • 深度优先遍历FiberTree
  • beginWorkcompleteWork工作逻辑。
  • wrokInProgree.tag根据不同的节点类型进行不同的处理。

只要你理解这一套运行逻辑:你就掌握了react源码中最核心的工作逻辑。

commitLayoutEffects_begin
// packages\react-reconciler\src\ReactFiberCommitWork.new.js

function commitLayoutEffects_begin(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  // Suspense layout effects semantics don't change for legacy roots.
  const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;

  // 循环触发
  while (nextEffect !== null) {
    // HostFiber
    const fiber = nextEffect;
    // fun App组件节点
    const firstChild = fiber.child;

    # 说明子树存在副作用,需要更新nextEffect为子节点,进入下一级的循环
    if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
      firstChild.return = fiber;
      nextEffect = firstChild;
    } else {
      # 说明副作用在自身节点了,进入complete阶段
      commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
    }
  }
}

可以看出这里的逻辑结构和BeforeMutation阶段完全一致,区别只是处理的内容不同。

commitLayoutMountEffects_complete

查看commitLayoutMountEffects_complete方法:

// packages\react-reconciler\src\ReactFiberCommitWork.new.js

function commitLayoutMountEffects_complete(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  // while循环
  while (nextEffect !== null) {
    const fiber = nextEffect;
    // 存在layout相关的副作用 才会进入
    if ((fiber.flags & LayoutMask) !== NoFlags) {
      const current = fiber.alternate;
      setCurrentDebugFiberInDEV(fiber);
      try {
        # 执行副作用
        commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
      } catch (error) {
        captureCommitPhaseError(fiber, fiber.return, error);
      }
    }

    # 当回到HostFiber时,代表循环完成,退出layout工作
    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }

    # 存在兄弟节点,开始兄弟节点的工作
    const sibling = fiber.sibling;
    if (sibling !== null) {
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }
	# 不存在兄弟节点,则返回父级节点
    nextEffect = fiber.return;
  }
}

依然是和BeforeMutation完全一致的循环逻辑,可以对比查看。

commitLayoutEffectOnFiber

继续查看commitLayoutEffectOnFiber方法内容:

// packages\react-reconciler\src\ReactFiberCommitWork.new.js

function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    # 根据组件类型:进行不同的处理
    switch (finishedWork.tag) {
      # 1,函数组件的处理
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
          // 处理副作用
          commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
        }
        break;
      }

      # 2,类组件的处理
      case ClassComponent: {
        // 组件实例
        const instance = finishedWork.stateNode;
        if (finishedWork.flags & Update) {
          if (!offscreenSubtreeWasHidden) {
            if (current === null) {
              // mount加载阶段
                
              # 触发componentDidMount 生命周期钩子函数【这类静态方法:是存储在instance对象原型上的】
              instance.componentDidMount();
            } else {
                
              // update更新阶段
              const prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps
                  : resolveDefaultProps( finishedWork.type, current.memoizedProps);
              const prevState = current.memoizedState;
              # 触发componentDidUpdate 生命周期钩子函数
              instance.componentDidUpdate( prevProps,prevState,
                  instance.__reactInternalSnapshotBeforeUpdate,
                );
            }
          }
        }
        
        # 取出当前组件节点的updateQueue更新对象
        const updateQueue: UpdateQueue = finishedWork.updateQueue;
        if (updateQueue !== null) {
          // 触发更新
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }
      case HostRoot: {
        // TODO: I think this is now always non-null by the time it reaches the
        // commit phase. Consider removing the type check.
        const updateQueue: UpdateQueue = finishedWork.updateQueue;
        if (updateQueue !== null) {
          let instance = null;
          if (finishedWork.child !== null) {
            switch (finishedWork.child.tag) {
              case HostComponent:
                instance = getPublicInstance(finishedWork.child.stateNode);
                break;
              case ClassComponent:
                instance = finishedWork.child.stateNode;
                break;
            }
          }
          // 触发更新
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }
      case HostComponent: {
        // 取出DOM对象
        const instance: Instance = finishedWork.stateNode;
        if (current === null && finishedWork.flags & Update) {
          const type = finishedWork.type;
          const props = finishedWork.memoizedProps;
          # 针对一些特殊的DOM元素做加载处理:button,input等做自动聚焦,对Img图片做加载
          commitMount(instance, type, props, finishedWork);
        }
        break;
      }
     
      ...

    }
  }
}

commitLayoutEffectOnFiber方法依然是和前面两个阶段的逻辑一样,根据不同的组件节点进行不同的逻辑处理,这里我们还是理解几个重点组件类型即可。

  • 函数组件
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);

执行commitHookEffectListMount方法,循环触发当前函数组件的useLayoutEffect 回调。

  • 类组件

根据当前Fiber节点是否存在current【旧的Fiber】来区分是mount阶段还是update阶段:

  • mount阶段:执行当前类组件的componentDidMount生命周期钩子函数。
  • update阶段:执行当前类组件的componentDidUpdate生命周期钩子函数。

最后执行当前类组件调用this.setState时传入的callback回调函数。

  • HostComponent

针对一些特殊的DOM元素做加载处理:比如input输入框做自动聚焦,对Img图片做加载。

Layout阶段总结

在真实DOM加载完成后,执行函数组件的Layout hook回调和类组件的生命周期钩子及setState相关回调。

4,FiberTree的切换

// 2,Mutation阶段
commitMutationEffects(root, finishedWork, lanes);
// FiberTree的切换
root.current = finishedWork;
// 3, Layout阶段
commitLayoutEffects(finishedWork, root, lanes);

Mutation阶段和Layout阶段之间还有一个重要处理没有说明,那就是FiberTree的切换。

通过前面我们已经知道,Mutation阶段处理完成之后,页面就已经完成了真实的DOM渲染。所以此时finishedWork就是最新的FiberTree以及存储着最新的DOM内容,在这里更新current的内容,主要有两个方面的作用:

  • 保留最新的Fiber树结构,在下一次更新时就可以利用本次的FiberTree来做数据的复用以及差异对比。
  • 对于类组件来说:当在Layout阶段执行componentDidMount或者componentDidUpdate生命周期钩子时就可以获取最新的DOM内容。

结束语

到此为止,一个react应用的整体加载流程算是解析完成了,其中有一些细节逻辑并没有展开讲解,比如类组件,函数组件的具体加载过程,hooks原理等等。所以后面新的章节会专注于这些细节逻辑的解析,觉得有帮助的朋友也可以继续多多关注。