流程概览一节我们了解组件在render阶段会经历beginWorkcompleteWork

上一节我们讲解了组件执行beginWork后会创建子Fiber节点,节点上可能存在flags

这一节让我们看看completeWork会做什么工作。

你可以从这里 (opens new window)看到completeWork方法定义。

# 流程概览

类似beginWorkcompleteWork也是针对不同fiber.tag调用不同的处理逻辑。

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IncompleteFunctionComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      bubbleProperties(workInProgress);
      return null;
    case ClassComponent: {
      // ...省略
      bubbleProperties(workInProgress);
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
    case HostHoistable:
    case HostSingleton:
    case ActivityComponent:
    case ViewTransitionComponent: {
      // ...省略
      return null;
    }
  // ...省略

随着SuspenseOffscreenCacheActivityViewTransition等能力加入,当前 React 源码中的completeWork分支比早期版本更多。我们重点关注页面渲染所必须的HostComponent(即原生DOM组件对应的Fiber节点),其他类型Fiber的处理留在具体功能实现时讲解。

# 处理 HostComponent

beginWork一样,我们根据current === null ?判断是mount还是update

同时针对HostComponent,判断update时我们还需要考虑workInProgress.stateNode != null ?(即该Fiber节点是否存在对应的DOM节点

case HostComponent: {
  popHostContext(workInProgress);
  const type = workInProgress.type;

  if (current !== null && workInProgress.stateNode != null) {
    // update的情况
    // ...省略
  } else {
    // mount的情况
    // ...省略
  }
  bubbleProperties(workInProgress);
  return null;
}

# update 时

update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:

  • onClickonChange等回调函数的注册
  • 处理style prop
  • 处理DANGEROUSLY_SET_INNER_HTML prop
  • 处理children prop

我们去掉一些当前不需要关注的功能(比如ref)。可以看到最主要的逻辑是调用updateHostComponent方法。

if (current !== null && workInProgress.stateNode != null) {
  // update的情况
  updateHostComponent(
    current,
    workInProgress,
    type,
    newProps,
    renderLanes
  );
}

你可以从这里 (opens new window)看到updateHostComponent方法定义。

在早期版本中,updateHostComponent会计算updatePayload并赋值给workInProgress.updateQueue,最终在commit阶段更新到页面上。

当前 React 19 中,updateHostComponent的逻辑更简单:如果新旧props相同,则直接返回;如果不同,则为当前Fiber节点标记Update flag

function updateHostComponent(
  current,
  workInProgress,
  type,
  newProps,
  renderLanes
) {
  const oldProps = current.memoizedProps;
  if (oldProps === newProps) {
    return;
  }

  markUpdate(workInProgress);
}

真正的DOM props diff会延后到commit阶段执行。对于ReactDOM,最终会在commitUpdate (opens new window)中调用updateProperties(domElement, type, oldProps, newProps)完成属性更新。

具体渲染过程见mutation 阶段一节

# mount 时

同样,我们省略了不相关的逻辑。可以看到,mount时的主要逻辑包括三个:

  • Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点
  • 处理初始props,必要时标记Update flag
// mount的情况

// ...省略服务端渲染 hydration 相关逻辑

const currentHostContext = getHostContext();
const rootContainerInstance = getRootHostContainer();

// 为fiber创建对应DOM节点
const instance = createInstance(
  type,
  newProps,
  rootContainerInstance,
  currentHostContext,
  workInProgress
);
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;

// 处理初始props,某些DOM节点需要在commit阶段执行额外操作,比如autoFocus
if (
  finalizeInitialChildren(
    instance,
    type,
    newProps,
    currentHostContext
  )
) {
  markUpdate(workInProgress);
}

还记得上一节我们讲到:mountChildFibers通常不会为新建子树中的每个后代都标记Placement flag。那么commit阶段是如何通过较少的插入DOM操作将整棵DOM树插入页面的呢?

原因就在于completeWork中的appendAllChildren方法。

由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到需要插入的顶层子树时,我们已经有一个构建好的离屏DOM树

初次渲染时,HostRoot因为存在alternate,会通过reconcileChildFibers创建顶层子Fiber节点,并为需要插入的顶层子树标记Placement flag。顶层子树内部的后续mount路径通常通过mountChildFibers创建,不会为每个后代都标记Placement flag。因此commit阶段处理顶层插入点时,可以把已经在completeWork中构建好的DOM树插入容器中。

# effectList

至此render阶段的绝大部分工作就完成了。

还有一个问题:作为DOM操作的依据,commit阶段需要找到所有有flagsFiber节点并依次执行flag对应操作。难道需要在commit阶段完整遍历一次Fiber树寻找flags !== NoFlagsFiber节点么?

这显然是很低效的。

在早期版本中,为了解决这个问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTagFiber节点会被保存在一条被称为effectList的单向链表中。

effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect

类似appendAllChildren,在“归”阶段,所有有effectTagFiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。

                       nextEffect         nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber

这样,在commit阶段只需要遍历effectList就能执行所有effect了。

你可以在这里 (opens new window)看到这段代码逻辑。

借用React团队成员Dan Abramov的话:effectList相较于Fiber树,就像圣诞树上挂的那一串彩灯。

不过,当前 React 19 已经不再使用firstEffectlastEffectnextEffect这套effectList字段。取而代之的是flagssubtreeFlags

completeWork的末尾,很多分支都会调用bubbleProperties(workInProgress)。这个方法会遍历当前Fiber节点的子节点,把子节点自身的flags与子树中的subtreeFlags向上冒泡:

let subtreeFlags = NoFlags;
let child = completedWork.child;

while (child !== null) {
  subtreeFlags |= child.subtreeFlags;
  subtreeFlags |= child.flags;
  child.return = completedWork;
  child = child.sibling;
}

completedWork.subtreeFlags |= subtreeFlags;

这样,commit阶段就可以通过subtreeFlags判断某棵子树是否包含需要处理的副作用。如果某棵子树的subtreeFlags不包含本阶段关心的flag,就可以跳过这棵子树;如果包含,再递归向下处理。比如mutation阶段会通过recursivelyTraverseMutationEffects遍历包含MutationMask的子树,并在具体节点上执行PlacementUpdateChildDeletion等操作。

所以,如果面向 React 19 理解这一段,可以将早期的effectList记为一个历史实现:它解决的是“如何快速找到有副作用的节点”的问题;当前实现用subtreeFlagsFiber树上保留同类信息,并让commit阶段按需递归遍历。

# 流程结尾

至此,render阶段全部工作完成。在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。

commitRoot(root);

代码见这里 (opens new window)

# 参考资料

completeWork流程图(基于早期源码绘制,理解主流程仍有参考价值)

completeWork流程图