beginWork 的第一站:HostRoot 如何把 App 接入 Fiber 树

上一篇我们讲到,React 进入 render 阶段后,会先通过:

复制代码
performWorkOnRoot
renderRootSync 或 renderRootConcurrent
prepareFreshStack
workLoopSync 或 workLoopConcurrent
performUnitOfWork
beginWork

正式开始处理 workInProgress tree。

这一篇继续往下走,重点只讲一个分支:

复制代码
HostRoot

也就是 beginWork 遇到根 Fiber 时会发生什么。

这篇是整个 React 初次渲染链路里非常关键的一篇:

root.render(<App />) 里的 App 到底什么时候变成 Fiber?

答案就在这一篇。

它不是在 createRoot 的时候变成 Fiber。

也不是在 root.render 的时候变成 Fiber。

也不是在 scheduleUpdateOnFiber 的时候变成 Fiber。

而是在 beginWork 处理 HostRootFiber 的时候,通过 updateHostRootprocessUpdateQueuereconcileChildren 这一套流程,才真正把 <App /> 接入 Fiber 树。

一、先回到 beginWork 的整体结构

React render 阶段的每一个 Fiber,都会进入:

复制代码
beginWork(current, workInProgress, renderLanes)

这三个参数分别是:

复制代码
current

当前已经提交生效的 Fiber。

复制代码
workInProgress

本轮 render 正在构建的 Fiber。

复制代码
renderLanes

本轮 render 正在处理的 lanes。

beginWork 会根据 workInProgress.tag 进入不同分支。

例如:

复制代码
FunctionComponent
ClassComponent
HostRoot
HostComponent
HostText
Fragment
SuspenseComponent
MemoComponent

在 React 源码中,beginWork 最终会通过 switch (workInProgress.tag) 分发到不同的 update 函数;其中 HostRoot 分支会调用 updateHostRoot(current, workInProgress, renderLanes)

大概结构可以理解成:

复制代码
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 不是专门处理函数组件的。

它是整个 render 阶段向下计算子 Fiber 的统一入口。

二、为什么第一个处理的是 HostRootFiber

prepareFreshStack 里,React 会创建本轮 render 的根工作节点:

复制代码
workInProgress = createWorkInProgress(root.current, null)

这里的 root.current 指向当前的 HostRootFiber

所以 work loop 一开始处理的就是:

复制代码
workInProgress HostRootFiber

也就是说,第一次进入 performUnitOfWork 时:

复制代码
unitOfWork = workInProgress HostRootFiber

然后:

复制代码
performUnitOfWork(unitOfWork)
beginWork(current, unitOfWork, renderLanes)

此时 workInProgress.tag 是:

复制代码
HostRoot

所以会进入:

复制代码
updateHostRoot(current, workInProgress, renderLanes)

这就是为什么 HostRoot 是 render 阶段的第一站。

不是因为你的 App 是根节点。

而是因为 React 内部真正的根节点永远是 HostRootFiber

App 只是挂在 HostRootFiber.child 上的第一个用户组件 Fiber。

三、HostRootFiber 和 App Fiber 不是一回事

这点必须分清楚。

你的代码是:

复制代码
const root = createRoot(document.getElementById('root'))

root.render(<App />)

这里有三个东西:

复制代码
DOM container

也就是:

复制代码
<div id="root"></div>

它是真实 DOM 容器。

复制代码
FiberRootNode

它是 React 应用级别的根对象,用来保存整个 Root 的调度状态、pendingLanes、callbackNode、finishedWork、containerInfo 等信息。

复制代码
HostRootFiber

它是 Fiber 树的根 Fiber。

它不是 App

它是 React 内部用来承载整个应用更新队列的根节点。

关系大概是:

复制代码
FiberRootNode.current = HostRootFiber
HostRootFiber.stateNode = FiberRootNode
FiberRootNode.containerInfo = DOM container

App Fiber 后面会挂到:

复制代码
HostRootFiber.child

所以初次渲染之前,Fiber 树大概是:

复制代码
FiberRootNode
  current -> HostRootFiber
              child -> null

调用 root.render(<App />) 后,App 还不是 Fiber。

它只是作为 update 的 payload 被挂到了 HostRootFiber.updateQueue 里。

只有 render 阶段处理 HostRootFiber 时,App 才会被取出来并转换成 Fiber。

四、root.render 做的事情再压缩一次

为了讲清楚 updateHostRoot,我们先把 root.render(<App />) 前面的事情压缩一下。

当你调用:

复制代码
root.render(<App />)

React 内部会创建一个 update。

这个 update 的核心内容大概是:

复制代码
update.payload = {
  element: <App />
}

然后这个 update 会进入:

复制代码
HostRootFiber.updateQueue

注意,这里存进去的还是 ReactElement。

也就是:

复制代码
{
  type: App,
  key: null,
  props: {},
  ref: null
}

它不是 Fiber。

所以在进入 render 阶段之前,React 只是完成了这件事:

复制代码
把 <App /> 作为一次更新,挂到 HostRootFiber.updateQueue 上

还没有创建 App Fiber

五、updateHostRoot 的核心职责

现在正式进入:

复制代码
updateHostRoot(current, workInProgress, renderLanes)

这个函数是 HostRootFiber 的 beginWork 处理逻辑。

它的核心职责可以拆成四步:

复制代码
pushHostRootContext
cloneUpdateQueue
processUpdateQueue
reconcileChildren

可以先用伪代码看一下:

复制代码
function updateHostRoot(current, workInProgress, renderLanes) {
  pushHostRootContext(workInProgress)

  const nextProps = workInProgress.pendingProps
  const prevState = workInProgress.memoizedState
  const prevChildren = prevState.element

  cloneUpdateQueue(current, workInProgress)
  processUpdateQueue(workInProgress, nextProps, null, renderLanes)

  const nextState = workInProgress.memoizedState
  const nextChildren = nextState.element

  reconcileChildren(
    current,
    workInProgress,
    nextChildren,
    renderLanes
  )

  return workInProgress.child
}

真实源码比这个复杂,因为还要处理 hydration、cache、transition tracing、suspense、error recovery 等逻辑。

但主线就是:

从 updateQueue 里计算出新的 root state。

从新的 root state 里拿到 element

把这个 element 交给 reconcile。

生成或复用子 Fiber。

React 源码中 updateHostRoot 会先 pushHostRootContext,然后读取 prevState.element,克隆并处理 updateQueue,之后从 nextState.element 得到 nextChildren

六、pushHostRootContext 是干嘛的

updateHostRoot 的第一步是:

复制代码
pushHostRootContext(workInProgress)

这个函数主要做两件事:

第一,处理顶层 context。

第二,压入宿主环境容器信息。

对于 ReactDOM 来说,这个容器就是:

复制代码
root.containerInfo

也就是你的真实 DOM 容器:

复制代码
<div id="root"></div>

为什么这里要 push host container?

因为后面到了 completeWork 创建真实 DOM 的时候,React 需要知道当前这棵 Fiber 子树对应哪个宿主容器。

React 不只支持 DOM。

它还可以支持 React Native、自定义 renderer 等。

所以 Reconciler 不应该直接写死 DOM 容器,而是通过 Host Config 和 Host Context 这套机制,把宿主环境信息维护起来。

对于你当前看 ReactDOM 来说,可以简单理解成:

pushHostRootContext 把根 DOM 容器压入上下文栈,后续 HostComponent 创建 DOM 和插入 DOM 时需要用到它。

七、prevState.element 是什么

updateHostRoot 里有这几行:

复制代码
const prevState = workInProgress.memoizedState
const prevChildren = prevState.element

这说明 HostRootFiber.memoizedState 里面保存了 root 的状态。

RootState 大概包含:

复制代码
{
  element,
  isDehydrated,
  cache
}

其中最关键的是:

复制代码
element

它代表当前 Root 已经渲染的 ReactElement。

初次渲染之前:

复制代码
prevState.element

通常是 null。

因为当前页面还没有渲染 <App />

调用 root.render(<App />) 后,新的 <App /> 不会马上写进 memoizedState

它先在 updateQueue 里。

processUpdateQueue 执行之后,新的 state 才会被计算出来。

所以初次渲染时大概是:

复制代码
prevState.element = null
update.payload.element = <App />
processUpdateQueue 后
nextState.element = <App />

这就是 App 从 updateQueue 进入 root state 的过程。

八、cloneUpdateQueue 为什么需要

updateHostRoot 里面会调用:

复制代码
cloneUpdateQueue(current, workInProgress)

为什么需要 clone?

因为 React 有 current tree 和 workInProgress tree 双缓存。

当前页面已经生效的是:

复制代码
current HostRootFiber

本轮正在计算的是:

复制代码
workInProgress HostRootFiber

updateQueue 是 Fiber 上的字段。

但是 render 阶段不能直接破坏 current 上的数据。

原因是 render 阶段可能被中断、重试、丢弃。

如果直接修改 current.updateQueue,那么一旦这次 render 没有提交,current tree 就被污染了。

所以 React 要把 current 上的 updateQueue 克隆到 workInProgress 上。

后续 processUpdateQueue 处理的是 workInProgress 的 queue。

这样即使这次 render 中断了,也不会破坏当前已经提交的 Fiber tree。

这就是双缓存模型的一部分。

一句话:

cloneUpdateQueue 是为了让本轮 render 可以在 workInProgress 上安全地计算更新,而不直接污染 current tree。

九、processUpdateQueue 如何拿到 App

接下来是最关键的一步:

复制代码
processUpdateQueue(workInProgress, nextProps, null, renderLanes)

这个函数会处理 workInProgress.updateQueue 里的更新。

对于 root.render(<App />) 这次更新来说,queue 里有一个 update,它的 payload 大概是:

复制代码
{
  element: <App />
}

处理之后,HostRootFiber.memoizedState 会变成新的 RootState。

也就是:

复制代码
workInProgress.memoizedState = {
  element: <App />,
  isDehydrated: false,
  cache: ...
}

然后源码继续读取:

复制代码
const nextState = workInProgress.memoizedState
const nextChildren = nextState.element

此时:

复制代码
nextChildren = <App />

这一步非常关键。

因为在这之前,<App /> 还藏在 updateQueue 里。

从这一刻开始,<App /> 被取出来,成为即将参与 reconcile 的 children。

React 源码中 updateHostRootprocessUpdateQueue 后读取 workInProgress.memoizedState,再通过 nextState.element 得到 nextChildren;源码注释还提到 DevTools 依赖这个字段名叫 element

十、nextChildren 为什么叫 children

你可能会疑惑:

<App /> 明明是根组件,为什么在 updateHostRoot 里叫:

复制代码
nextChildren

原因是从 HostRootFiber 的视角看,App 确实是它的 child。

Fiber 树的结构是:

复制代码
HostRootFiber
  child -> App Fiber

所以对于 HostRootFiber 来说:

复制代码
<App />

就是它的 children。

同理,对于:

复制代码
function App() {
  return <div>Hello</div>
}

当 React 处理 App Fiber 时,App 函数返回的:

复制代码
<div>Hello</div>

就是 App Fiber 的 nextChildren。

所以你可以形成一个统一理解:

每个 Fiber 的 beginWork,本质上都是计算它下一层的 children。

对于 HostRoot,children 来自 updateQueue 里的 root element。

对于 FunctionComponent,children 来自函数组件执行后的返回值。

对于 HostComponent,children 来自 props.children。

十一、reconcileChildren:App 进入 Fiber 树的入口

拿到:

复制代码
nextChildren = <App />

之后,React 会调用:

复制代码
reconcileChildren(
  current,
  workInProgress,
  nextChildren,
  renderLanes
)

这一步才是 ReactElement 转 Fiber 的入口。

在 React 源码中,reconcileChildren 会根据 current 是否为 null 选择不同逻辑:如果是新挂载,走 mountChildFibers;如果是更新,走 reconcileChildFibers

源码结构大概是:

复制代码
export function reconcileChildren(
  current,
  workInProgress,
  nextChildren,
  renderLanes
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes
    )
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes
    )
  }
}

这里要注意一个细节。

初次渲染时,HostRootFibercurrent 不是 null。

因为在 createRoot 的时候,React 已经创建了 current HostRootFiber。

所以对 HostRoot 来说:

复制代码
current !== null

但是:

复制代码
current.child === null

因为 App 还没有挂上去。

所以初次渲染 HostRoot 的 reconcile 实际上会进入:

复制代码
reconcileChildFibers(
  workInProgress,
  current.child, // null
  nextChildren, // <App />
  renderLanes
)

虽然走的是 reconcileChildFibers,但因为旧 child 是 null,本质上还是创建新 child Fiber。

这点比简单说"初次渲染一定走 mountChildFibers"更准确。

对于 HostRoot 的初次渲染:

current 不为 null。

current.child 为 null。

所以它会通过 reconcileChildFibers 创建 App Fiber。

十二、reconcileChildFibers 做什么

reconcileChildFibers 的职责是比较旧 child 和新 children,然后返回新的 child Fiber。

它要处理很多情况:

复制代码
单个 ReactElement
多个数组 children
文本节点
Fragment
Portal
Lazy
Context

对于最简单的:

复制代码
root.render(<App />)

nextChildren 是一个单个 ReactElement。

所以会进入类似:

复制代码
reconcileSingleElement

然后根据 element 创建 Fiber:

复制代码
createFiberFromElement

继续往下会走到:

复制代码
createFiberFromTypeAndProps

也就是根据:

复制代码
element.type
element.key
element.props

创建对应 Fiber。

对于:

复制代码
<App />

它的 type 是:

复制代码
App

如果 App 是函数组件,那么创建出来的 Fiber tag 大概率就是:

复制代码
FunctionComponent

于是得到:

复制代码
App Fiber

然后把它挂到:

复制代码
workInProgress.child = appFiber
appFiber.return = workInProgress

也就是:

复制代码
HostRootFiber.child = App Fiber
AppFiber.return = HostRootFiber

从这一刻开始,App 才真正进入 Fiber 树。

十三、为什么说 App 是在 HostRoot 的 beginWork 中变成 Fiber 的

把前面的过程合起来看:

复制代码
root.render(<App />)

只是创建 update:

复制代码
update.payload.element = <App />

然后挂到:

复制代码
HostRootFiber.updateQueue

直到 render 阶段:

复制代码
beginWork HostRootFiber
updateHostRoot
processUpdateQueue
nextState.element = <App />
nextChildren = <App />
reconcileChildren
reconcileChildFibers
createFiberFromElement
createFiberFromTypeAndProps
App Fiber 创建完成

所以严格来说:

App 是在 updateHostRoot 中被取出,在 reconcileChildren 中被转换成 Fiber。

更完整一点:

App 从 ReactElement 到 Fiber 的转换,发生在 HostRootFiber 的 beginWork 阶段。

这就是你写博客时应该强调的结论。

十四、beginWork 返回 workInProgress.child 的意义

updateHostRoot 最后会返回:

复制代码
return workInProgress.child

此时 workInProgress.child 已经是:

复制代码
App Fiber

所以 performUnitOfWork 会拿到这个返回值:

复制代码
const next = beginWork(current, unitOfWork, renderLanes)

if (next === null) {
  completeUnitOfWork(unitOfWork)
} else {
  workInProgress = next
}

因为 next 不为 null,所以:

复制代码
workInProgress = App Fiber

下一轮 work loop 就会处理 App Fiber

于是调用链变成:

复制代码
performUnitOfWork HostRootFiber
beginWork HostRootFiber
updateHostRoot
reconcileChildren 创建 App Fiber
return App Fiber

performUnitOfWork App Fiber
beginWork App Fiber
updateFunctionComponent
renderWithHooks
执行 App 函数
拿到 App 返回的 ReactElement
继续 reconcileChildren

所以 HostRoot 的 beginWork 只是把根组件接入 Fiber 树。

真正执行 App 函数组件,是下一轮处理 App Fiber 的时候。

这一点也很重要。

updateHostRoot 不会执行 App()

它只是创建 App Fiber

App() 是在 updateFunctionComponent 里通过 renderWithHooks 执行的。

十五、HostRoot 初次渲染和后续更新有什么不同

初次渲染:

复制代码
prevState.element = null
nextState.element = <App />
current.child = null

所以 reconcile 的结果是创建新的 App Fiber

后续更新可能是:

复制代码
root.render(<NewApp />)

或者同一个 App 内部触发状态更新。

如果是再次调用 root.render:

复制代码
prevState.element = <App />
nextState.element = <NewApp />
current.child = 旧 App Fiber

这时候 reconcile 会比较旧 child 和新 element。

如果 type 和 key 相同,就复用 Fiber:

复制代码
current App Fiber
workInProgress App Fiber

如果 type 或 key 不同,就删除旧 Fiber,创建新 Fiber。

这就是 reconcile 的核心规则。

React 官方旧版 reconciliation 文档也强调,React 的 diff 算法基于两个假设:不同类型的元素会产生不同树,开发者可以通过 key 提示哪些子元素在不同渲染中保持稳定。

对于根节点来说也是一样。

如果你从:

复制代码
root.render(<App />)

变成:

复制代码
root.render(<OtherApp />)

那么根下面的 child type 变了,React 会认为这是不同的树,旧的 App 子树会被卸载,新的 OtherApp 子树会被创建。

十六、HostRoot 为什么也有 updateQueue

很多人会以为只有组件才有 updateQueue。

比如类组件的 setState

但其实 HostRootFiber 也有 updateQueue。

因为 root.render(<App />) 本质上就是对 Root 的一次更新。

从 React 视角看:

复制代码
root.render(<App />)

不是直接渲染 App。

而是更新 Root 的 element 状态。

Root 的状态从:

复制代码
{
  element: null
}

变成:

复制代码
{
  element: <App />
}

这个状态变化必须经过 updateQueue。

所以 HostRoot 也需要 updateQueue。

你可以把 HostRoot 的 updateQueue 理解成:

保存 root.render 产生的根更新。

保存 hydrate 相关更新。

保存 error recovery 相关更新。

保存某些顶层状态变化。

对于初次渲染,我们只需要关注:

复制代码
payload.element = <App />

这一条更新。

十七、为什么 updateHostRoot 还要处理 cache 和 hydration

你看源码时会发现,updateHostRoot 里不只有 updateQueue 和 reconcile,还有很多看起来很干扰主线的东西:

复制代码
pushRootTransition
pushCacheProvider
propagateContextChange
suspendIfUpdateReadFromEntangledAsyncAction
supportsHydration
isDehydrated
mountHostRootWithoutHydrating
enterHydrationState

这些不是初学主线必须深挖的内容。

但你写开源博客时可以简单提一下,显得你不是只会讲伪代码。

这些逻辑主要服务于 React 18 之后的并发能力、Suspense、Cache、SSR hydration、transition tracing 等机制。

对于本文主线来说,可以先这样理解:

updateHostRoot 不只是"拿 App 创建 Fiber",它还负责建立根节点相关的运行上下文,比如宿主容器、顶层 context、cache、hydration 状态等。

但是如果只看客户端初次渲染,主线仍然是:

复制代码
processUpdateQueue
nextState.element
reconcileChildren

十八、为什么 HostRoot 的 current 不应该是 null

在源码里有这段逻辑:

复制代码
if (current === null) {
  throw new Error('Should have a current fiber. This is a bug in React.')
}

这说明对于 HostRoot 来说,React 期望 current 永远存在。

为什么?

因为 createRoot 阶段已经创建了:

复制代码
FiberRootNode
HostRootFiber

并且建立了:

复制代码
root.current = hostRootFiber

所以到 render 阶段处理 HostRoot 时,一定有 current HostRootFiber。

即使是初次渲染,也不是没有 current tree。

只是 current tree 还没有子节点。

这也是 React 双缓存模型里很容易误解的一点:

初次渲染不是没有 current tree。

初次渲染有 current tree,只是它是一棵空壳 root tree。

它大概是:

复制代码
current HostRootFiber
  child: null

本轮 render 会创建:

复制代码
workInProgress HostRootFiber
  child: App Fiber

commit 后:

复制代码
root.current = finishedWork

于是包含 App 的 workInProgress tree 变成新的 current tree。

十九、把 HostRoot 的 beginWork 画成一条链路

这一篇的核心链路可以这样记:

复制代码
performUnitOfWork(HostRootFiber)

beginWork(
  current HostRootFiber,
  workInProgress HostRootFiber,
  renderLanes
)

switch workInProgress.tag

case HostRoot:
  updateHostRoot(current, workInProgress, renderLanes)

pushHostRootContext(workInProgress)

prevState = workInProgress.memoizedState
prevChildren = prevState.element

cloneUpdateQueue(current, workInProgress)

processUpdateQueue(workInProgress, nextProps, null, renderLanes)

nextState = workInProgress.memoizedState
nextChildren = nextState.element

reconcileChildren(
  current,
  workInProgress,
  nextChildren,
  renderLanes
)

reconcileChildFibers(
  workInProgress,
  current.child,
  nextChildren,
  renderLanes
)

createFiberFromElement(<App />)

生成 App Fiber

workInProgress.child = App Fiber
App Fiber.return = workInProgress HostRootFiber

return workInProgress.child

下一轮 workLoop 处理 App Fiber

这条链路就是:

App 从 updateQueue 进入 Fiber 树的完整过程。

二十、这一篇最重要的结论

第一,beginWork 是 render 阶段向下计算子 Fiber 的入口。

第二,render 阶段第一个处理的 Fiber 是 HostRootFiber,因为 prepareFreshStackroot.current 创建 workInProgress 根节点。

第三,HostRootFiber 不是 App Fiber,它是 React 内部 Fiber 树的根。

第四,root.render(<App />) 不会直接创建 App Fiber,它只是创建一个 update,并把 <App /> 放进 update.payload.element

第五,这个 update 会挂到 HostRootFiber.updateQueue 上。

第六,updateHostRoot 会通过 processUpdateQueue 处理 updateQueue,计算出新的 memoizedState

第七,nextState.element 就是本轮 Root 要渲染的 children,也就是 <App />

第八,reconcileChildren 会把 <App /> 交给 child reconciler。

第九,child reconciler 会通过 createFiberFromElementcreateFiberFromTypeAndProps 创建 App Fiber

第十,App Fiber 会挂到 workInProgress HostRootFiber.child 上。

第十一,updateHostRoot 最后返回 workInProgress.child,所以 work loop 下一轮会处理 App Fiber

第十二,updateHostRoot 不会执行 App(),真正执行函数组件是在下一篇要讲的 updateFunctionComponentrenderWithHooks

相关推荐
我命由我123451 小时前
Dart - Dart SDK、Hello World 案例、变量声明、常量声明、常量 final、字符串类型
前端·flutter·前端框架·html·web·dart·web app
冴羽yayujs1 小时前
GitHub 前端热榜项目 - 日榜(2026-05-11)
前端·github
~|Bernard|1 小时前
四,go语言中GMP调度模型
java·前端·golang
YOU OU1 小时前
HTML+CSS+JavaScript
前端·javascript·css·html
Rkgua2 小时前
路径传参和查询传参和请求体传参区以及Vue和React的用法区分
前端·面试
JarvanMo2 小时前
Flutter + Supabase 集成 Apple Sign-In 完整指南
前端
小村儿2 小时前
连载
前端·后端·ai编程
dinl_vin2 小时前
LangChain 系列·(九):Agent——让 AI 自己做决策
前端·人工智能·langchain
孟祥_成都2 小时前
前端唯一的护城河?结合 AI 将字节组件库 Headless 化后的感想~
前端·人工智能·react.js