从 performWorkOnRoot 到 workInProgress tree:React 真正开始 render 的地方
上一篇我们讲到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) } }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 treecurrent tree和workInProgress treecurrent 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 / }于是会执行 reconcilereconcileChildren(current, workInProgress, nextChildren, renderLanes)这时候才会创建App Fiber并挂到workInProgress.child也就是挂到 workInProgress tree 上。初次渲染时current tree 的HostRootFiber.child还是 null。workInProgress tree 的HostRootFiber.child会变成 App Fiber。等 commit 完成后root.current finishedWorkworkInProgress 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) }每处理完一个 FiberperformUnitOfWork会返回下一个要处理的 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 nextwork 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 /)会创建一个 updateupdate.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 Fiberwork 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 nullwork 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负责向下计算子 FibercompleteWork负责向上完成节点并收集副作用。第十render 阶段的产物是一棵 finished workInProgress tree以及上面标记好的 flags真正修改 DOM 要等 commit 阶段。