上一篇我们讲到,scheduleUpdateOnFiber 并不会直接进入 beginWork,它只是把这次更新标记到 Root 上,然后通过 Root 调度系统决定后续怎么执行。
在 React 19 中,这条链路大概是:
scheduleUpdateOnFiber
markRootUpdated
ensureRootIsScheduled
processRootScheduleInMicrotask
scheduleTaskForRootDuringMicrotask
performSyncWorkOnRoot 或 performWorkOnRootViaSchedulerTask
performWorkOnRoot
这一篇我们就从 performWorkOnRoot 开始讲。
也就是 React 真正进入 render 阶段的地方。
这篇要解决几个问题:
performWorkOnRoot 到底做了什么?
React 是怎么决定走同步渲染还是并发渲染的?
workInProgress tree 是什么时候创建的?
beginWork 是怎么被调用起来的?
初次渲染时,App 是在哪里变成 Fiber 的?
一、先明确 performWorkOnRoot 的位置
前面我们已经知道,React 19 中同步任务和并发任务会从不同入口进来。
同步任务一般会进入:
performSyncWorkOnRoot(root, lanes)
并发任务一般会从 Scheduler 回调进入:
performWorkOnRootViaSchedulerTask(root, didTimeout)
但是它们最终都会走到:
performWorkOnRoot(root, lanes, forceSync)
所以 performWorkOnRoot 才是 render 阶段真正的总入口。
你可以这样理解:
scheduleUpdateOnFiber 负责告诉 React 有更新了。
ensureRootIsScheduled 负责把 Root 放进调度系统。
scheduleTaskForRootDuringMicrotask 负责判断这个 Root 应该怎么被调度。
performWorkOnRoot 才负责真正开始处理这个 Root 上的更新。
React 19 源码中,performWorkOnRootViaSchedulerTask 是通过 Scheduler 进入 React work loop 的入口,它会重新计算 lanes,然后调用 performWorkOnRoot(root, lanes, forceSync) 进入实际渲染流程。
二、performWorkOnRoot 不是直接 beginWork
很多人以为:
performWorkOnRoot
beginWork
completeWork
commitRoot
这个理解太粗了。
真实流程中,performWorkOnRoot 在进入 beginWork 之前,还要做几件非常关键的事情:
performWorkOnRoot
判断是否需要同步渲染
判断是否需要时间切片
调用 renderRootSync 或 renderRootConcurrent
prepareFreshStack
创建 workInProgress tree 的根节点
进入 workLoopSync 或 workLoopConcurrent
performUnitOfWork
beginWork
completeUnitOfWork
completeWork
所以 performWorkOnRoot 不是"遍历 Fiber 树"的函数。
它更像是 render 阶段的控制器。
它要先判断这次渲染怎么跑,然后才会进入真正的 work loop。
三、performWorkOnRoot 先决定同步还是并发
React 内部有两种 render 方式:
renderRootSync
和:
renderRootConcurrent
同步渲染就是一口气把 work loop 跑完,中途不会因为时间片耗尽而主动让出主线程。
并发渲染则会在 work loop 中检查是否应该让出主线程。
这就是 React 并发渲染的基础。
大概逻辑可以理解成:
function performWorkOnRoot(root, lanes, forceSync) {
const shouldTimeSlice =
!forceSync &&
!includesBlockingLane(lanes) &&
!includesExpiredLane(root, lanes)
const exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes)
if (exitStatus !== RootInProgress) {
commit 或继续处理异常、挂起、重试等情况
}
}
这段伪代码表达的是核心思想,不是源码逐字复刻。
关键点在于:
React 不是只看你是不是 Concurrent Root。
它还要看当前 lanes 的优先级、是否过期、是否被强制同步执行。
也就是说,并发 Root 上也可能跑同步渲染。
比如:
用户触发了高优先级同步更新。
某些 lane 已经过期。
flushSync 强制同步执行。
这些情况下,即使 Root 支持并发能力,React 也可能走 renderRootSync。
React work loop 中会根据是否应该 time slice,在 renderRootConcurrent 和 renderRootSync 之间选择;并发渲染并不是永远并发,它会受到 lanes、超时、强制同步等条件影响。
四、renderRootSync 和 renderRootConcurrent 的区别
这两个函数的目标是一样的:
从 Root 开始构建 workInProgress tree。
区别在于 work loop 的执行方式不同。
renderRootSync
同步渲染大概是这样:
function renderRootSync(root, lanes) {
prepareFreshStack(root, lanes)
do {
try {
workLoopSync()
break
} catch (thrownValue) {
handleThrow(root, thrownValue)
}
} while (true)
return workInProgressRootExitStatus
}
核心是:
workLoopSync()
它会一直执行,直到没有下一个工作单元:
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress)
}
}
同步模式下,只要开始构建,就会一直往下跑,不主动让出主线程。
renderRootConcurrent
并发渲染大概是这样:
function renderRootConcurrent(root, lanes) {
prepareFreshStack(root, lanes)
do {
try {
workLoopConcurrent()
break
} catch (thrownValue) {
handleThrow(root, thrownValue)
}
} while (true)
return workInProgressRootExitStatus
}
核心是:
workLoopConcurrent()
它会在每个工作单元之间判断是否应该让出主线程:
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress)
}
}
这就是你之前问的 shouldYield() 的意义。
workLoop 本身当然是在 JS 主线程上执行的。
如果一次 render 工作太多,React 不能一直霸占主线程,否则浏览器没有机会处理用户输入、动画、布局、绘制等任务。
所以并发渲染里,React 会把大的渲染任务拆成一个个 Fiber 工作单元。
每处理一个 Fiber,就检查一下:
现在还能继续干吗?
如果时间片用完了,就先停下来。
下次 Scheduler 再回调时,继续从上次停下的 workInProgress 开始干。
这就是并发渲染可中断、可恢复的基础。
五、prepareFreshStack:创建 workInProgress tree 的入口
不管是 renderRootSync 还是 renderRootConcurrent,在进入 work loop 之前,都要先调用:
prepareFreshStack(root, lanes)
这个函数非常关键。
因为它会创建本轮 render 所使用的 workInProgress tree 的根节点。
大概逻辑是:
function prepareFreshStack(root, lanes) {
root.finishedWork = null
root.finishedLanes = NoLanes
workInProgressRoot = root
workInProgressRootRenderLanes = lanes
workInProgressRootExitStatus = RootInProgress
workInProgress = createWorkInProgress(root.current, null)
}
重点是这一句:
workInProgress = createWorkInProgress(root.current, null)
root.current 指向当前页面已经生效的 Fiber tree。
createWorkInProgress(root.current, null) 会基于 current tree 创建一棵新的 workInProgress tree 的根。
所以 workInProgress tree 不是凭空出现的。
它是从 current tree 克隆出来的。
不过这里要注意:
初次渲染时,也有 current tree。
只是这棵 current tree 非常空。
它只有一个 HostRootFiber,还没有真正的 App 子 Fiber。
所以初次渲染时:
root.current
指向的是 HostRootFiber。
它的 child 是 null。
然后 prepareFreshStack 会基于这个 HostRootFiber 创建对应的 workInProgress 版本。
也就是:
current HostRootFiber
workInProgress HostRootFiber
接下来进入 beginWork 时,React 才会从 HostRootFiber.updateQueue 里拿到 payload.element,也就是 <App />,然后通过 reconcile 创建 App 对应的 Fiber。
所以你前面问的那个问题可以在这里得到准确答案:
App 不是在 root.render 时变成 Fiber 的。
也不是在 scheduleUpdateOnFiber 时变成 Fiber 的。
它是在 render 阶段处理 HostRootFiber 的 beginWork 时,通过 reconcileChildren 变成 Fiber 的。
六、current tree 和 workInProgress tree 到底是什么关系
React 内部始终维护两棵 Fiber tree:
current tree
和:
workInProgress tree
current tree 是当前屏幕上已经提交生效的 Fiber tree。
workInProgress tree 是本轮 render 正在计算的新 Fiber tree。
它们之间通过 alternate 互相连接。
currentFiber.alternate === workInProgressFiber
workInProgressFiber.alternate === currentFiber
初次渲染时:
current HostRootFiber
workInProgress HostRootFiber
这两个根 Fiber 已经通过 alternate 关联。
但是 App 对应的 Fiber 还没有。
因为 current tree 里还没有 App。
当 beginWork 处理 HostRootFiber 时,React 发现 updateQueue 里有:
{
element: <App />
}
于是会执行 reconcile:
reconcileChildren(current, workInProgress, nextChildren, renderLanes)
这时候才会创建:
App Fiber
并挂到:
workInProgress.child
也就是挂到 workInProgress tree 上。
初次渲染时,current tree 的 HostRootFiber.child 还是 null。
workInProgress tree 的 HostRootFiber.child 会变成 App Fiber。
等 commit 完成后:
root.current = finishedWork
workInProgress tree 就会变成新的 current tree。
这就是双缓存模型。
七、createWorkInProgress 做了什么
createWorkInProgress(current, pendingProps) 的作用是基于 current Fiber 创建或复用它的 alternate。
大概逻辑是:
function createWorkInProgress(current, pendingProps) {
let workInProgress = current.alternate
if (workInProgress === null) {
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode
)
workInProgress.elementType = current.elementType
workInProgress.type = current.type
workInProgress.stateNode = current.stateNode
workInProgress.alternate = current
current.alternate = workInProgress
} else {
workInProgress.pendingProps = pendingProps
workInProgress.flags = NoFlags
workInProgress.subtreeFlags = NoFlags
workInProgress.deletions = null
}
workInProgress.childLanes = current.childLanes
workInProgress.lanes = current.lanes
workInProgress.child = current.child
workInProgress.memoizedProps = current.memoizedProps
workInProgress.memoizedState = current.memoizedState
workInProgress.updateQueue = current.updateQueue
return workInProgress
}
这个函数有两个分支。
第一次创建 alternate
如果 current 没有 alternate,就创建一个新的 Fiber。
这通常发生在某个 Fiber 第一次拥有 workInProgress 对应节点时。
后续复用 alternate
如果 current 已经有 alternate,就复用它。
复用时会重置:
flags
subtreeFlags
deletions
因为这些副作用标记属于上一轮 render,不能污染这一轮。
这也是 Fiber 架构性能优化的重要点:
React 不会每次都从零创建整棵树。
它会复用 alternate 结构,在 current 和 workInProgress 之间来回切换。
八、workInProgress 是一个全局游标
进入 render 阶段后,React 内部有一个非常关键的变量:
workInProgress
它不是某个 Fiber 的属性,而是 React work loop 当前正在处理的 Fiber 指针。
可以把它理解成 DFS 遍历中的当前节点。
一开始:
workInProgress = workInProgressRootFiber
也就是 workInProgress 版本的 HostRootFiber。
然后 work loop 开始:
while (workInProgress !== null) {
performUnitOfWork(workInProgress)
}
每处理完一个 Fiber,performUnitOfWork 会返回下一个要处理的 Fiber。
如果当前 Fiber 有子节点,就进入子节点。
如果没有子节点,就 complete 当前节点,然后找兄弟节点。
如果没有兄弟节点,就一路向上 complete 父节点。
所以 React render 阶段本质上是一次深度优先遍历。
只不过它不是递归写法,而是用 workInProgress 这个全局游标实现的可中断遍历。
为什么不能简单用递归?
因为递归一旦开始,浏览器很难在中途恢复到某个精确的 Fiber 节点。
React 要支持并发渲染,就必须知道:
当前做到哪个 Fiber 了。
下次恢复时从哪里继续。
所以 Fiber 本身就是为可中断渲染设计的数据结构。
九、performUnitOfWork:每个 Fiber 的工作入口
work loop 每次都会调用:
performUnitOfWork(unitOfWork)
它大概长这样:
function performUnitOfWork(unitOfWork) {
const current = unitOfWork.alternate
let next = beginWork(current, unitOfWork, renderLanes)
unitOfWork.memoizedProps = unitOfWork.pendingProps
if (next === null) {
completeUnitOfWork(unitOfWork)
} else {
workInProgress = next
}
}
这个函数非常重要,因为它连接了两个阶段:
beginWork
completeWork
你可以这样理解:
beginWork 是向下走。
completeWork 是向上归。
beginWork 返回子节点
如果当前 Fiber 处理完之后还有 child 需要继续处理,beginWork 会返回 child。
然后:
workInProgress = next
work loop 下一轮就处理这个 child。
beginWork 返回 null
如果当前 Fiber 没有子节点,或者子节点不需要处理,beginWork 返回 null。
这时候说明不能继续向下了,要开始完成当前 Fiber。
于是进入:
completeUnitOfWork(unitOfWork)
completeUnitOfWork 会调用 completeWork,然后找 sibling 或 return。
十、beginWork 是干什么的
beginWork(current, workInProgress, renderLanes) 的核心职责是:
根据当前 Fiber 类型,计算它的子 Fiber。
不同类型的 Fiber,处理方式不一样。
比如:
HostRoot
FunctionComponent
ClassComponent
HostComponent
HostText
Fragment
SuspenseComponent
MemoComponent
ForwardRef
它们都会在 beginWork 里面进入不同分支。
大概结构是:
function beginWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes)
case FunctionComponent:
return updateFunctionComponent(current, workInProgress, Component, nextProps, renderLanes)
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes)
case HostText:
return null
}
}
所以 beginWork 不是只负责函数组件。
它是所有 Fiber 类型进入 render 计算的分发入口。
React 的 beginWork 会根据 Fiber tag 分发到不同更新函数;work loop 通过 performUnitOfWork 调用 beginWork,如果返回子节点就继续向下,否则进入完成阶段。
十一、初次渲染时 App 是怎么变成 Fiber 的
这是这一篇最关键的部分。
以:
root.render(<App />)
为例。
前面已经知道,root.render(<App />) 会创建一个 update:
update.payload = {
element: <App />
}
这个 update 会挂到:
HostRootFiber.updateQueue
等到 render 阶段开始时,第一个被处理的 Fiber 是:
workInProgress HostRootFiber
于是进入:
beginWork(current, workInProgress, renderLanes)
因为它的 tag 是 HostRoot,所以会进入:
updateHostRoot(current, workInProgress, renderLanes)
updateHostRoot 会处理 updateQueue。
处理完之后,会得到:
nextChildren = <App />
然后执行:
reconcileChildren(current, workInProgress, nextChildren, renderLanes)
也就是:
reconcileChildren(
currentHostRootFiber,
workInProgressHostRootFiber,
<App />,
renderLanes
)
这时候 React 才真正根据 <App /> 这个 ReactElement 创建 App Fiber。
大概过程是:
<App /> 是 ReactElement
reconcileSingleElement
createFiberFromElement
createFiberFromTypeAndProps
生成 App 对应的 Fiber
workInProgressHostRootFiber.child = appFiber
appFiber.return = workInProgressHostRootFiber
所以完整链路是:
root.render(<App />)
创建 update
update.payload.element = <App />
update 挂到 HostRootFiber.updateQueue
scheduleUpdateOnFiber
performWorkOnRoot
renderRootSync 或 renderRootConcurrent
prepareFreshStack
workInProgress = workInProgress HostRootFiber
performUnitOfWork
beginWork HostRootFiber
updateHostRoot
processUpdateQueue
拿到 nextChildren,也就是 <App />
reconcileChildren
createFiberFromElement
创建 App Fiber
挂到 workInProgress.child
这才是 App 从 ReactElement 进入 Fiber 体系的准确位置。
十二、为什么 App 不在 root.render 时就变成 Fiber
因为 root.render 属于更新创建阶段。
它只负责表达:
我要把这个 element 渲染到这个 root 里。
它不会立即计算 Fiber 树。
原因有三个。
第一,React 需要先调度。
这次更新可能是同步的,也可能是并发的。
在调度系统决定执行之前,React 不应该直接开始构建 Fiber。
第二,React 需要批处理。
如果同一个事件里连续发生多次更新,React 会先把它们合并到 Root 上,而不是每次都立即构建一遍 Fiber tree。
第三,React 需要可中断渲染。
Fiber tree 的构建属于 render 阶段,它可能被暂停、恢复、重试、丢弃。
所以 ReactElement 到 Fiber 的转换,必须发生在 render work loop 里面,而不是 update 创建时。
这也是 Fiber 架构和老的同步递归渲染模型很大的区别。
十三、beginWork 为什么叫 begin
因为它只做当前 Fiber 的开始工作。
具体来说,它主要负责:
计算当前 Fiber 的新状态。
执行组件函数或处理 updateQueue。
生成或复用子 Fiber。
决定是否可以 bailout。
返回下一个要处理的子 Fiber。
它不负责创建真实 DOM。
真实 DOM 的创建主要发生在 completeWork 阶段。
比如对于原生节点:
<div />
在 beginWork 阶段,React 只是创建或复用它的 Fiber 子节点。
到了 completeWork 阶段,才会为 HostComponent 创建真实 DOM 实例。
所以 render 阶段可以分成两个方向:
向下的 begin 阶段:
beginWork
计算子 Fiber
向上的 complete 阶段:
completeWork
创建 DOM 或收集副作用
这也是为什么 performUnitOfWork 里面先调用 beginWork。
只有当当前节点没有 child 可以继续向下时,才开始 completeUnitOfWork。
十四、workLoop 的遍历过程
假设组件结构是:
function App() {
return (
<div>
<Header />
<Content />
</div>
)
}
初次渲染时,Fiber 构建过程大概是:
HostRootFiber
App Fiber
div Fiber
Header Fiber
Content Fiber
work loop 的执行顺序大概是:
beginWork HostRootFiber
创建 App Fiber
beginWork App Fiber
执行 App 函数组件
得到 div ReactElement
创建 div Fiber
beginWork div Fiber
处理 children
创建 Header Fiber 和 Content Fiber
beginWork Header Fiber
执行 Header 函数组件
创建 Header 的子 Fiber
如果 Header 没有更多 child
completeWork Header
beginWork Content Fiber
执行 Content 函数组件
创建 Content 的子 Fiber
completeWork Content
completeWork div
创建 div DOM,挂载子 DOM
completeWork App
completeWork HostRootFiber
render 阶段完成
这个过程是深度优先的。
先一路 begin 到最深。
然后 complete 当前节点。
再找兄弟节点。
兄弟节点处理完,再回到父节点 complete。
十五、completeUnitOfWork 是怎么向上归的
当 beginWork 返回 null 时,React 会进入:
completeUnitOfWork(unitOfWork)
它大概做这几件事:
function completeUnitOfWork(unitOfWork) {
let completedWork = unitOfWork
do {
const current = completedWork.alternate
const returnFiber = completedWork.return
completeWork(current, completedWork, renderLanes)
const siblingFiber = completedWork.sibling
if (siblingFiber !== null) {
workInProgress = siblingFiber
return
}
completedWork = returnFiber
workInProgress = completedWork
} while (completedWork !== null)
}
它的逻辑是:
完成当前 Fiber。
如果有 sibling,就处理 sibling。
如果没有 sibling,就回到 parent。
一直往上归。
当最终归到 HostRootFiber,并且也没有更多 sibling 时,整棵 workInProgress tree 就构建完成了。
这时候:
workInProgress = null
work loop 结束。
十六、render 阶段结束后得到了什么
render 阶段结束后,React 并没有马上修改页面。
它只是得到了一个计算完成的 workInProgress tree。
这个 tree 上有:
新的 memoizedProps。
新的 memoizedState。
新的 child Fiber 结构。
需要执行的 flags。
子树上的 subtreeFlags。
需要删除的 deletions。
对于初次渲染来说,很多 Fiber 会带有 Placement 标记。
表示这些节点需要在 commit 阶段插入到真实 DOM 中。
对于更新来说,可能会有:
Update
Placement
ChildDeletion
Ref
Passive
Layout
等 flags。
所以 render 阶段的产物不是 DOM 更新本身,而是一份"待提交的变更计划"。
真正修改 DOM,要等 commit 阶段。
十七、为什么 render 阶段可以被中断,但 commit 阶段不行
这个问题很重要。
render 阶段做的是计算。
它在构建 workInProgress tree。
如果中途被打断,还没有影响真实页面。
所以它可以暂停、恢复、重试,甚至丢弃。
比如低优先级渲染做到一半,用户突然输入文字,React 可以先暂停低优先级任务,处理高优先级输入。
但是 commit 阶段不一样。
commit 阶段会真正修改 DOM。
一旦开始插入、删除、更新 DOM,就不能随便中断。
否则页面可能处在半更新状态。
所以 React 的并发能力主要发生在 render 阶段。
commit 阶段仍然是同步执行的。
这也是理解 Concurrent Rendering 的关键:
Concurrent 不是说 DOM 提交也可以被随意中断。
它主要是说 render 计算阶段可以被调度、暂停和恢复。
React 的并发渲染核心在于 render 阶段可中断,而 commit 阶段负责应用已经计算好的变更,需要保持同步一致性。
十八、这一篇的完整调用链
把这一篇串起来,React 19 中从调度进入 render 的主线是:
performWorkOnRootViaSchedulerTask
或者 performSyncWorkOnRoot
performWorkOnRoot(root, lanes, forceSync)
判断 shouldTimeSlice
如果需要同步渲染
renderRootSync(root, lanes)
如果可以并发渲染
renderRootConcurrent(root, lanes)
prepareFreshStack(root, lanes)
workInProgress = createWorkInProgress(root.current, null)
进入 workLoopSync 或 workLoopConcurrent
performUnitOfWork(workInProgress)
beginWork(current, workInProgress, renderLanes)
如果是 HostRoot
updateHostRoot
processUpdateQueue
拿到 nextChildren
reconcileChildren
创建 App Fiber
如果是 FunctionComponent
updateFunctionComponent
renderWithHooks
执行函数组件
拿到 children
reconcileChildren
如果是 HostComponent
updateHostComponent
处理 props.children
reconcileChildren
如果 beginWork 返回 child
workInProgress = child
继续向下
如果 beginWork 返回 null
completeUnitOfWork
completeWork
找 sibling 或 return
直到 workInProgress === null
render 阶段完成
root.finishedWork = workInProgressRoot
进入 commitRoot
十九、这一篇最重要的结论
第一,performWorkOnRoot 是 React 真正进入 render 阶段的总入口。
第二,performWorkOnRoot 不会直接调用 beginWork,它会先决定本轮是同步渲染还是并发渲染。
第三,同步渲染走 renderRootSync,会一直执行 work loop,直到整棵 workInProgress tree 构建完成。
第四,并发渲染走 renderRootConcurrent,会在 work loop 中通过 shouldYield() 判断是否让出主线程。
第五,prepareFreshStack 会基于 root.current 创建本轮渲染的 workInProgress 根节点。
第六,初次渲染时,current tree 不是不存在,而是只有一个空的 HostRootFiber。
第七,App 不是在 root.render 时变成 Fiber 的,而是在 beginWork HostRootFiber 时,通过 updateHostRoot、processUpdateQueue、reconcileChildren 变成 App Fiber 的。
第八,work loop 是基于 workInProgress 指针实现的深度优先遍历,不是普通递归。
第九,beginWork 负责向下计算子 Fiber,completeWork 负责向上完成节点并收集副作用。
第十,render 阶段的产物是一棵 finished workInProgress tree,以及上面标记好的 flags,真正修改 DOM 要等 commit 阶段。