在流程概览一节我们了解组件在render阶段会经历beginWork与completeWork。
上一节我们讲解了组件执行beginWork后会创建子Fiber节点,节点上可能存在flags。
这一节让我们看看completeWork会做什么工作。
你可以从这里 (opens new window)看到completeWork方法定义。
# 流程概览
类似beginWork,completeWork也是针对不同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;
}
// ...省略
随着Suspense、Offscreen、Cache、Activity、ViewTransition等能力加入,当前 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,比如:
onClick、onChange等回调函数的注册- 处理
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阶段需要找到所有有flags的Fiber节点并依次执行flag对应操作。难道需要在commit阶段完整遍历一次Fiber树寻找flags !== NoFlags的Fiber节点么?
这显然是很低效的。
在早期版本中,为了解决这个问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。
effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。
类似appendAllChildren,在“归”阶段,所有有effectTag的Fiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。
nextEffect nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber
这样,在commit阶段只需要遍历effectList就能执行所有effect了。
你可以在这里 (opens new window)看到这段代码逻辑。
借用React团队成员Dan Abramov的话:effectList相较于Fiber树,就像圣诞树上挂的那一串彩灯。
不过,当前 React 19 已经不再使用firstEffect、lastEffect、nextEffect这套effectList字段。取而代之的是flags与subtreeFlags。
在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的子树,并在具体节点上执行Placement、Update、ChildDeletion等操作。
所以,如果面向 React 19 理解这一段,可以将早期的effectList记为一个历史实现:它解决的是“如何快速找到有副作用的节点”的问题;当前实现用subtreeFlags在Fiber树上保留同类信息,并让commit阶段按需递归遍历。
# 流程结尾
至此,render阶段全部工作完成。在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。
commitRoot(root);
# 参考资料
completeWork流程图(基于早期源码绘制,理解主流程仍有参考价值)
