上一篇我们讲到,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 的时候,通过 updateHostRoot、processUpdateQueue、reconcileChildren 这一套流程,才真正把 <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 源码中 updateHostRoot 在 processUpdateQueue 后读取 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
)
}
}
这里要注意一个细节。
初次渲染时,HostRootFiber 的 current 不是 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,因为 prepareFreshStack 从 root.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 会通过 createFiberFromElement、createFiberFromTypeAndProps 创建 App Fiber。
第十,App Fiber 会挂到 workInProgress HostRootFiber.child 上。
第十一,updateHostRoot 最后返回 workInProgress.child,所以 work loop 下一轮会处理 App Fiber。
第十二,updateHostRoot 不会执行 App(),真正执行函数组件是在下一篇要讲的 updateFunctionComponent 和 renderWithHooks。