前言
本文的React代码版本为18.2.0
可调试的代码仓库为:GitHub - yyyao-hh/react-debug at master-pure
上一节讲了Fiber的结构和概念,要真正理解React的并发渲染机制,我们必须深入剖析Fiber树的构建过程。本文将从源码层面,完整解析React在Render阶段如何构建Fiber树。
Fiber 树的构建入口
多余代码均被省略,仅保留核心逻辑
我们从入口文件开始分析:首先创建一个根容器,然后对应用进行挂载
ini
/* index.js */
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
然后在render方法的内部调用了updateContainer方法启动了渲染流程
javascript
/* react/packages/react-dom/src/client/ReactDOMRoot.js */
ReactDOMRoot.prototype.render = function(children: ReactNodeList) {
const root = this._internalRoot; // 根节点
updateContainer(children, root, null, null);
};
scss
/* react/packages/react-reconciler/src/ReactFiberReconciler.old.js */
export function updateContainer(...) {
const root = enqueueUpdate(current, update, lane);
scheduleUpdateOnFiber(root, current, lane, eventTime);
}
scss
/* react/packages/react-reconciler/src/ReactFiberWorkLoop.old.js */
export function scheduleUpdateOnFiber(...) {
// 注册调度任务, 之后由 Scheduler 调度, 构造 Fiber 树
ensureRootIsScheduled(root, eventTime);
}
ensureRootIsScheduled函数中通过scheduleCallback、scheduleSyncCallback、scheduleLegacySyncCallback等方法注册了两种类型的调度任务。然后调度任务中就是Fiber树的构建逻辑。
- 同步渲染:
performSyncWorkOnRoot - 并发渲染:
performConcurrentWorkOnRoot
本章无需关心任务的调度,在调度器章节会详细分析。我们只需要了解在注册任务后,任务会在某个时机会被调度器(
Scheduler)执行。
scss
/* react/packages/react-reconciler/src/ReactFiberWorkLoop.old.js */
function ensureRootIsScheduled(...) {
// 1. 同步任务处理
if (newCallbackPriority === SyncLane) {
if (root.tag === LegacyRoot) {
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
// 2. 并发任务处理
} else {
// ...计算优先级的逻辑
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
}
而render阶段正是从performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的调用开始。取决于本次是同步更新还是异步更新。但不论是哪种情况,最终都会调用本章的重点:performUnitOfWork方法
performSyncWorkOnRoot => renderRootSync => workLoopSyncperformConcurrentWorkOnRoot => renderRootConcurrent => workLoopConcurrent
scss
/* packages\react-reconciler\src\ReactFiberWorkLoop.old.js */
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
这两个函数唯一的区别就是是否调用了shouldYield方法:判断是否应中断当前任务。
在之后的调度器章节会详细分析。
Fiber 树的构建过程
performUnitOfWork函数是Fiber架构中工作循环(work loop)中的核心函数,负责对单个Fiber节点进行处理并驱动整个渲染流程。其核心作用是协调"递 "和"归"两个阶段,实现可中断的深度优先遍历。
javascript
/* packages\react-reconciler\src\ReactFiberWorkLoop.old.js */
let workInProgress: Fiber | null = null;
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork: Fiber): void {
const next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
Fiber树的遍历采用深度优先遍历,在遍历的过程中:
workInProgress:指向当前正在处理的Fiber节点的指针。若指针为null,表示所有工作单元已处理完毕,结束循环performUnitOfWork:创建下一个Fiber节点并赋值给指针workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树next:下一个需要处理的Fiber节点。先从beginWork中获取,如果没有,就从completeUnitOfWork获取。
beginWork是"递",即深度优先遍历找到当前分支最深叶子节点的过程;
completeUnitOfWork是"归",即结束这个分支,向右或向上的过程。
然后接下来分别深入了解beginWork和completeUnitOfWork这两个方法
递 - beginWork
首先从RootFiberNode开始进行深度优先遍历。遍历到的每个节点都会调用beginWork方法。该方法为传入的Fiber节点创建子Fiber节点,并将两个节点进行关联,然后返回子Fiber节点。
当返回的子Fiber节点为空(叶子节点),这时就会进入"归"阶段。
beginWork方法内部主要是根据tag分发逻辑,处理不同类型的Fiber节点。将处理方法的返回的子节点作为下一个遍历节点(熔断策略会另起一篇文章讲解)
kotlin
/* packages/react-reconciler/src/ReactFiberBeginWork.old.js */
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if(...) {...}; // 满足一定条件下执行熔断策略
switch (workInProgress.tag) {
case IndeterminateComponent: return mountIndeterminateComponent(...);
case LazyComponent: return mountLazyComponent(...);
case FunctionComponent: return updateFunctionComponent(...);
case ClassComponent: return updateClassComponent(...);
case HostRoot: return updateHostRoot(...);
case HostComponent: return updateHostComponent(...);
case HostText: return updateHostText(...);
case SuspenseComponent: return updateSuspenseComponent(...);
case HostPortal: return updatePortalComponent(...);
case ForwardRef: return updateForwardRef(...);
case Fragment: return updateFragment(...);
case Mode: return updateMode(...);
case Profiler: return updateProfiler(...);
case ContextProvider: return updateContextProvider(...);
case ContextConsumer: return updateContextConsumer(...);
case MemoComponent: return updateMemoComponent(...);
case SimpleMemoComponent: return updateSimpleMemoComponent(...);
case IncompleteClassComponent: return mountIncompleteClassComponent(...);
case SuspenseListComponent: return updateSuspenseListComponent(...);
case ScopeComponent: return updateScopeComponent(...);
case OffscreenComponent: return updateOffscreenComponent(...);
case LegacyHiddenComponent: return updateLegacyHiddenComponent(...);
case CacheComponent: return updateCacheComponent(...);
case TracingMarkerComponent: return updateTracingMarkerComponent(...);
}
}
可以简单看下对类组件(ClassComponent)和函数式组件(FunctionComponent)的处理。可以看出每个函数最终都会返回workInProgress.child:因为遵循深度优先,返回节点即为当前节点的第一个子节点
javascript
// 类组件 (ClassComponent)
function finishClassComponent(...) {
return workInProgress.child;
}
// 函数式组件 (FunctionComponent)
function updateFunctionComponent(...) {
const nextUnitOfWork = finishClassComponent(...);
return nextUnitOfWork;
}
function finishClassComponent(...) {
return workInProgress.child;
}
归 - completeUnitOfWork
当返回的子Fiber节点为空,就会继续调用completeUnitOfWork函数进行"归"阶段的处理。completeUnitOfWork函数内部也有一层循环,并搭配了一个新的向上的指针。判断:
- 如果该节点有兄弟
Fiber节点,就会改变workInProgress,并跳出函数内部的循环,进入兄弟Fiber节点的"递"阶段。 - 如果该节点无兄弟
Fiber节点,指针就会指向父节点,就会进入父级Fiber节点的"归"阶段。
ini
/* packages/react-reconciler/src/ReactFiberWorkLoop.old.js */
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const returnFiber = completedWork.return;
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}
这个"递"和"归"的处理相互交错,直到最终回到RootFiberNode。循环结束!
completeUnitOfWork阶段还会构建effect链表,为后续的commit阶段提供精确的DOM操作指令。但这个会在之后的commit阶段中展开详细讲解。
在这个例子中:
- 首先进入根节点
A1,执行beginWork方法。返回其子节点B1,继续循环; - 进入
B1,执行beginWork方法。但是B1没有子节点返回,继续执行completeUnitOfWork方法。
-
- 该节点有兄弟节点,因此使
workInProgress指针指向其兄弟节点B2,继续外层循环;
- 该节点有兄弟节点,因此使
- 进入
B2,执行beginWork方法。返回其子节点C1,继续循环; - 进入
C1,执行beginWork方法。但是C1没有子节点返回,继续执行completeUnitOfWork方法。
-
- 该节点无兄弟节点,因此使方法内部指针指向其父节点
B2,继续方法内部循环; B2节点继续内部循环,判断B2有兄弟节点,因此使workInProgress指针指向其兄弟节点B3,继续外层循环;
- 该节点无兄弟节点,因此使方法内部指针指向其父节点
- 进入
B3,执行beginWork方法。返回其子节点C2,继续循环; - 进入
C2,执行beginWork方法。返回其子节点D1,继续循环; - 进入
D1,执行beginWork方法。但是D1没有子节点返回,继续执行completeUnitOfWork方法。
-
- 该节点有兄弟节点,因此使
workInProgress指针指向其兄弟节点D2,继续外层循环;
- 该节点有兄弟节点,因此使
- 进入
D2,执行beginWork方法。但是D2没有子节点返回,继续执行completeUnitOfWork方法。
-
- 该节点无兄弟节点,因此使方法内部指针指向其父节点
C2,继续方法内部循环; C2节点继续内部循环,判断C2无兄弟节点,因此使方法内部指针指向其父节点B3,继续方法内部循环;B3节点继续内部循环,判断B3无兄弟节点,因此使方法内部指针指向其父节点A1,继续方法内部循环;A1节点继续内部循环,判断A1无兄弟节点无父节点,因此内层循环结束,外层循环结束;
- 该节点无兄弟节点,因此使方法内部指针指向其父节点

workInProgress指针的变化(遵循深度优先):A1、B1、B2、C1、B3、C2、D1、D2、A1
completeUnitOfWork指针的变化(根据workInProgress指针的变化):
B1->B2:B1C1->B3:C1、B2D1->D2:D1D2->A1:D2、C2、B3、A1
其实
completeUnitOfWork函数内部的指针completedWork在兄弟节点之间并未跳转(比如B2->B3),此处连接只是为了方便大家理解~
总结
Render阶段的核心要点(本章只详细讲解了第一条,其他慢慢补坑):
- 深度优先遍历:
Fiber树的构建采用DFS策略,确保父节点在子节点之前开始,在子节点之后完成。 - 可中断渲染:
React的并发特性建立在可中断的Fiber架构之上,通过shouldYield控制渲染任务的中断和恢复。 - 副作用收集:
completeUnitOfWork阶段构建effect链表,为后续的commit阶段提供精确的DOM操作指令。 - 高效协调:通过
key优化、多轮diff算法和bailout机制,最大限度地复用现有节点。
下一章我们将了解Fiber树构建过程中的复用策略:Diff算法