从 createRoot 到 root.render:React 更新链路的起点到底做了什么
一、为什么现在入口变成了 createRoot
在 React 18 之前,应用入口通常是这样写的:
import ReactDOM from 'react-dom'
ReactDOM.render(<App />, document.getElementById('root'))
这个 API 的表达很直接:
把 App 渲染到 container 里面。
但是从 React 18 开始,官方推荐写法变成了:
import { createRoot } from 'react-dom/client'
const root = createRoot(document.getElementById('root'))
root.render(<App />)
表面上看,只是把一个函数调用拆成了两步。
旧写法:
ReactDOM.render(element, container)
新写法:
const root = createRoot(container)
root.render(element)
但这不是简单的 API 形式调整。
这背后对应的是 React 内部模型的变化。
React 不再把一次渲染看成一个孤立动作,而是先为某个容器创建一个长期存在的 Root,然后所有更新都提交到这个 Root 上,再由 Root 统一管理调度、优先级、渲染过程和提交过程。
也就是说,React 18 之后的入口更接近 React 内部真实模型:
先创建根容器
再向根容器提交更新
这就是 createRoot 和 root.render 被拆开的原因。
二、旧 API 的问题不在功能,而在模型表达
ReactDOM.render 并不是不能用,它的问题主要是语义太扁平。
它看起来像是:
render(element, container)
这会给人一种错觉:
调用 render 之后,React 立刻把 element 渲染成 DOM。
但从 React 内部看,这件事远比这个 API 表达得复杂。
React 至少要维护这些信息:
当前已经显示在页面上的 Fiber 树
正在构建中的 workInProgress 树
当前 root 上有哪些待处理更新
这些更新分别是什么优先级
当前是否已经安排了调度任务
这次 render 完成后的 finishedWork 是什么
是否存在挂起的 Suspense
是否存在 hydration 状态
是否有未提交的副作用
这些状态不是一次 render(element, container) 可以表达清楚的。
React 需要一个长期存在的对象来保存这些 root 级别的信息。
这个对象就是 FiberRootNode。
而 createRoot(container) 的核心意义就是:
为这个 container 创建一个 React 内部的 FiberRootNode,并把它包装成用户能操作的 ReactDOMRoot。
所以新 API 的重点不是多了一层写法,而是把 Root 这个概念显式暴露了出来。
三、先区分两个 Root:ReactDOMRoot 和 FiberRootNode
看源码时,最容易混的是两个 Root。
一个是用户拿到的 root:
const root = createRoot(container)
这个 root 是 ReactDOMRoot 实例。
另一个是 React 内部真正管理更新的 root:
FiberRootNode
它们不是同一个东西。
可以这样理解:
ReactDOMRoot 是暴露给用户的壳
FiberRootNode 是 React 内部真正的根容器
简化结构如下:
function ReactDOMRoot(internalRoot) {
this._internalRoot = internalRoot
}
也就是说:
root._internalRoot
指向的就是内部的 FiberRootNode。
用户调用:
root.render(<App />)
本质上就是通过这个用户层 root,找到内部的 FiberRootNode,然后往这个内部 root 上提交更新。
这个分层很重要。
ReactDOMRoot 负责暴露 API。
FiberRootNode 负责管理更新系统。
四、createRoot 的主线调用
从用户代码开始:
const root = createRoot(container)
在 React DOM 内部,大体会进入:
createRoot(container, options)
核心流程可以压缩成:
function createRoot(container, options) {
const root = createContainer(
container,
ConcurrentRoot,
null,
false,
null,
'',
onUncaughtError,
onCaughtError,
onRecoverableError,
null
)
return new ReactDOMRoot(root)
}
源码细节会因为版本不同略有差异,但主线稳定:
createRoot
createContainer
createFiberRoot
createHostRootFiber
new ReactDOMRoot
也就是说,createRoot 做的不是渲染,而是创建根。
它不会执行 App。
它不会生成 AppFiber。
它不会创建 DOM。
它主要完成两件事:
第一,创建 FiberRootNode。
第二,创建 HostRootFiber,并让二者互相连接。
五、createContainer 创建的是 FiberRootNode
createRoot 内部会调用 reconciler 暴露出来的 createContainer。
简化理解:
export function createContainer(containerInfo, tag, hydrationCallbacks, isStrictMode) {
return createFiberRoot(
containerInfo,
tag,
hydrate,
initialChildren,
hydrationCallbacks,
isStrictMode
)
}
createFiberRoot 会创建 React 内部真正的根对象。
简化结构如下:
function FiberRootNode(containerInfo, tag) {
this.tag = tag
this.containerInfo = containerInfo
this.current = null
this.pendingLanes = NoLanes
this.suspendedLanes = NoLanes
this.pingedLanes = NoLanes
this.finishedLanes = NoLanes
this.callbackNode = null
this.callbackPriority = NoLane
this.finishedWork = null
}
这里每个字段都不是摆设。
先抓几个关键字段。
1. containerInfo
this.containerInfo = containerInfo
它保存真实 DOM 容器。
例如:
document.getElementById('root')
这个字段告诉 React:
最终 DOM 要插入到哪里。
注意,FiberRootNode 本身不是 DOM。
它只是持有 DOM 容器引用。
2. current
this.current = null
后面会指向 HostRootFiber。
这是当前已经生效的 Fiber 树的根节点。
3. pendingLanes
this.pendingLanes = NoLanes
表示这个 root 上有哪些待处理更新。
React 不是只知道“要更新”,而是要知道:
有哪些优先级的更新要处理
这个信息会记录在 root 的 lanes 上。
4. callbackNode 和 callbackPriority
this.callbackNode = null
this.callbackPriority = NoLane
这两个字段和调度有关。
如果某个 root 已经被 Scheduler 安排了任务,React 会把这个调度任务记录在 root 上,避免重复安排相同优先级的任务。
5. finishedWork
this.finishedWork = null
当 render 阶段完成后,React 会得到一棵完整的 workInProgress 树。
这棵树会暂时挂到:
root.finishedWork
后续 commit 阶段会提交它。
所以 FiberRootNode 是 root 级别状态中心。
它负责保存整个应用根上的运行状态。
六、HostRootFiber 是 Fiber 树的真正起点
创建 FiberRootNode 之后,React 会创建一个特殊 Fiber:
const uninitializedFiber = createHostRootFiber(tag, isStrictMode)
这个 Fiber 的 tag 是:
HostRoot
它不是你写的 App。
它是 React 内部为每个 root 创建的根 Fiber。
可以理解为:
HostRootFiber 是用户组件树之上的那个内部根节点
关系如下:
FiberRootNode
current 指向 HostRootFiber
HostRootFiber
stateNode 指回 FiberRootNode
源码里会建立这种双向关系:
root.current = uninitializedFiber
uninitializedFiber.stateNode = root
这一步非常关键。
此时 React 内部已经有了根结构:
FiberRootNode
HostRootFiber
但是还没有:
AppFiber
divFiber
spanFiber
真实 DOM 子节点
因为此时只是执行了:
createRoot(container)
还没有执行:
root.render(<App />)
所以 createRoot 的产物不是完整 Fiber 树,而是一个空的根。
准确说,是一个已经初始化好的 HostRootFiber 和 FiberRootNode。
七、为什么需要 HostRootFiber,而不是直接让 App 当根
你可能会问:
既然最终要渲染的是 App,为什么不直接让 AppFiber 当根?
为什么还要多一个 HostRootFiber?
原因是 root 本身也需要参与更新系统。
root.render(<App />) 本质上也是一次更新。
这次更新没有发生在 App 上,因为 AppFiber 此时还不存在。
它只能发生在一个已经存在的 Fiber 上。
这个已经存在的 Fiber 就是 HostRootFiber。
也就是说:
HostRootFiber 是 root 更新的承载点。
它的 updateQueue 用来保存 root.render 传进来的 element。
第一次渲染时:
root.render(<App />)
会生成一个 update,挂到 HostRootFiber 上。
后续再次调用:
root.render(<OtherApp />)
也会生成新的 update,仍然挂到 HostRootFiber 上。
所以 HostRootFiber 的作用是:
承载 root 级别更新。
作为 Fiber 树遍历的起点。
连接 FiberRootNode 和用户组件树。
八、root.render 的入口
现在进入第二步:
root.render(<App />)
用户拿到的 root 是 ReactDOMRoot 实例。
它的 render 方法大致如下:
ReactDOMRoot.prototype.render = function(children) {
const root = this._internalRoot
if (root === null) {
throw new Error('Cannot update an unmounted root.')
}
updateContainer(children, root, null, null)
}
这里的 children 就是你传入的 ReactElement。
例如:
<App />
此时它大概是:
{
type: App,
key: null,
props: {},
ref: null
}
注意,这里仍然只是 ReactElement。
它还没有变成 AppFiber。
root.render 自己也不负责把它转成 Fiber。
它只是把这个 ReactElement 交给:
updateContainer
从这里开始,进入 React 更新系统。
九、root.render 的本质是创建一次更新
updateContainer 是理解 root.render 的关键。
简化版本如下:
function updateContainer(element, container, parentComponent, callback) {
const current = container.current
const lane = requestUpdateLane(current)
const update = createUpdate(lane)
update.payload = {
element
}
if (callback !== null) {
update.callback = callback
}
const root = enqueueUpdate(current, update, lane)
if (root !== null) {
scheduleUpdateOnFiber(root, current, lane)
}
return lane
}
这段代码就是 root.render 的核心。
逐行拆。
十、第一步:拿到 HostRootFiber
const current = container.current
这里的 container 是 FiberRootNode。
所以:
container.current
拿到的是 HostRootFiber。
这说明 root.render 的 update 不是挂到 App 上。
因为 AppFiber 还不存在。
它会挂到 HostRootFiber 上。
这也是为什么前面必须先讲 createRoot。
如果没有 createRoot 创建 HostRootFiber,root.render 这次更新就没有承载点。
所以入口链路是:
createRoot 创建 HostRootFiber
root.render 把更新挂到 HostRootFiber
这才是完整模型。
十一、第二步:申请 lane
const lane = requestUpdateLane(current)
React 中每次更新都会被分配一个 lane。
lane 表示这次更新的优先级和批次信息。
不要把 lane 简单理解成数字。
它本质上是一个位掩码。
不同二进制位代表不同优先级或不同更新通道。
例如可以抽象理解为:
SyncLane
DefaultLane
TransitionLane
IdleLane
root.render(<App />) 不是只创建更新内容,还会给这次更新分配优先级。
这是 React 18 之后非常重要的统一模型。
无论是:
root.render(<App />)
还是:
setState(...)
还是:
startTransition(...)
最终都会创建带 lane 的 update。
React 后面就是根据 lane 决定:
这次更新什么时候执行
是否可以被打断
是否可以和其他更新合并
当前 render 要处理哪些更新
所以从 updateContainer 开始,React 就把“更新内容”和“更新优先级”绑定在一起了。
十二、第三步:创建 update
const update = createUpdate(lane)
update 是 React 对“一次变更”的内部表示。
简化结构:
function createUpdate(lane) {
return {
lane,
tag: UpdateState,
payload: null,
callback: null,
next: null
}
}
对 root.render 来说,这个 update 表示:
root 的 element 要更新了。
对 useState 来说,update 表示:
某个 Hook 的 state 要更新了。
它们不是完全相同的数据结构路径,但设计思想是统一的:
React 不会立刻修改状态或立刻构建 UI。
React 会先创建 update,把更新记录下来。
之后 render 阶段统一处理 updateQueue。
这就是 React 能实现批处理、优先级调度和并发渲染的基础。
十三、第四步:把 ReactElement 放进 payload
update.payload = {
element
}
这是 root.render 最关键的一行。
用户传入的 <App /> 到这里没有被执行,也没有被转换成 Fiber。
它只是被保存到了 update.payload 里面。
也就是说,ReactElement 此时的位置变成了:
HostRootFiber.updateQueue 中某个 update 的 payload.element
可以把这一步理解为:
ReactElement 从“用户传入的参数”,变成了“HostRootFiber 的待处理更新内容”。
这是它进入 React 更新系统的真正方式。
所以不能说:
root.render 直接把 ReactElement 转成 Fiber。
更准确的是:
root.render 把 ReactElement 包装成 update,然后把 update 提交到 HostRootFiber。
十四、第五步:enqueueUpdate 把 update 入队
const root = enqueueUpdate(current, update, lane)
这里的 current 是 HostRootFiber。
所以 update 会进入 HostRootFiber 的 updateQueue。
HostRootFiber 初始化时就会有 updateQueue。
简化理解:
hostRootFiber.updateQueue = {
baseState: {
element: null
},
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null
},
callbacks: null
}
当 root.render 产生 update 后,会被挂到 shared.pending 上。
更新队列不是随便设计的。
因为同一个 Fiber 上可能连续产生多个 update。
例如:
root.render(<App1 />)
root.render(<App2 />)
或者组件中:
setCount(1)
setCount(2)
setCount(3)
React 需要先把这些 update 存起来,然后在 render 阶段按照 lane 和顺序计算最终结果。
对 HostRootFiber 来说,最终要算出来的是:
新的 root state
这个 state 里最重要的是:
element
也就是当前 root 应该渲染哪个 ReactElement。
十五、updateQueue 在 render 阶段才会被消费
root.render 阶段只是入队。
真正消费 updateQueue 是在 render 阶段处理 HostRootFiber 时。
后面会进入类似逻辑:
processUpdateQueue(workInProgress, nextProps, null, renderLanes)
处理完之后,HostRootFiber 的 memoizedState 会得到新的 state。
对于 HostRootFiber 来说,state 大概是:
{
element: <App />
}
然后 React 才会取出:
const nextChildren = workInProgress.memoizedState.element
再进入:
reconcileChildren(current, workInProgress, nextChildren, renderLanes)
也就是说:
ReactElement 真正被拿出来构建子 Fiber,是后面的 render 阶段。
不是 root.render 阶段。
这条边界一定要清楚。
root.render 阶段:
ReactElement 进入 update.payload
render 阶段:
processUpdateQueue 取出 element
reconcileChildren 把 element 转成 Fiber
十六、第六步:scheduleUpdateOnFiber 通知 React 有更新
update 入队之后,React 还要调度这次更新。
所以会调用:
scheduleUpdateOnFiber(root, current, lane)
这个函数名字很关键:
不是 performUpdate。
不是 renderRoot。
而是 scheduleUpdate。
它的语义是:
这个 Fiber 上有更新,把它标记到 root,并确保 root 之后会被执行。
简化逻辑可以理解为:
function scheduleUpdateOnFiber(root, fiber, lane) {
markRootUpdated(root, lane)
ensureRootIsScheduled(root)
}
实际源码更复杂,会处理 render phase update、interleaved update、suspended render 等情况,但主线就是这两件事。
第一,把 lane 标记到 root。
第二,确保 root 被调度。
十七、markRootUpdated:把 lane 标到 root 上
markRootUpdated(root, lane)
它会更新 root 上的 pendingLanes。
简化理解:
root.pendingLanes |= lane
这表示:
这个 root 上现在有一个 lane 对应的更新等待处理。
为什么 lane 要标在 root 上?
因为 React 调度的单位是 root。
一个页面可以有多个 root:
const root1 = createRoot(container1)
const root2 = createRoot(container2)
每个 root 都有自己的 pendingLanes、callbackNode、finishedWork。
React 要知道:
哪个 root 有任务。
这个 root 上最高优先级任务是什么。
应该给这个 root 安排什么调度回调。
所以 update 发生在 Fiber 上,但最终要把信息汇总到 FiberRootNode 上。
十八、普通 setState 为什么还要向上找 root
root.render 的 update 直接发生在 HostRootFiber 上,所以 root 很容易找到。
但普通组件更新可能发生在很深的 Fiber 上。
例如:
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
点击时,更新发生在 CounterFiber 上。
React 必须从 CounterFiber 一路向上找到 HostRootFiber,再找到 FiberRootNode。
这就是类似 markUpdateLaneFromFiberToRoot 这类逻辑存在的原因。
它会沿着 return 指针向上走:
let node = sourceFiber
let parent = node.return
while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane)
node = parent
parent = node.return
}
if (node.tag === HostRoot) {
return node.stateNode
}
这里就用到了 Fiber 的 return 指针。
这也说明 Fiber 结构和调度系统是连在一起的。
Fiber 不是孤立的数据结构。
lane 需要沿 Fiber 树传播。
root 需要通过 Fiber 找到。
调度需要依赖 root 的 pendingLanes。
十九、ensureRootIsScheduled:确保 root 会被执行
有了 pendingLanes 之后,React 还要决定:
这个 root 是否需要安排新的调度任务。
这就是:
ensureRootIsScheduled(root)
它大概会做几件事:
检查 root 上是否有待处理 lanes
找出最高优先级 lanes
判断现有 callback 是否可以复用
如果不能复用,取消旧 callback
根据优先级安排新的 callback
简化逻辑:
function ensureRootIsScheduled(root) {
const existingCallbackNode = root.callbackNode
const nextLanes = getNextLanes(root)
if (nextLanes === NoLanes) {
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode)
}
root.callbackNode = null
root.callbackPriority = NoLane
return
}
const newCallbackPriority = getHighestPriorityLane(nextLanes)
if (root.callbackPriority === newCallbackPriority) {
return
}
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode)
}
const newCallbackNode = scheduleCallback(
schedulerPriority,
performConcurrentWorkOnRoot.bind(null, root)
)
root.callbackNode = newCallbackNode
root.callbackPriority = newCallbackPriority
}
这段逻辑的核心不是“立即执行更新”,而是:
给这个 root 安排一个未来要执行的任务。
同步更新可能会被同步 flush。
并发更新会交给 Scheduler。
但不管哪种,root.render 到这里仍然没有进入 beginWork。
它只是把 root 放进了调度体系。
二十、为什么 root.render 不直接进入 render 阶段
这是一个非常关键的设计点。
如果 root.render 直接执行 render 阶段,看起来会更简单:
root.render(<App />)
App()
createDOM()
appendChild()
但这样 React 就失去了这些能力:
批量更新
优先级控制
中断恢复
多 root 调度
Suspense 挂起恢复
Transition 降低优先级
错误恢复
渲染阶段重试
React 的设计不是“调用一次就马上干完”。
而是:
先把更新记录下来。
再根据优先级决定什么时候处理。
处理时构建 workInProgress 树。
构建完成后统一 commit。
所以 root.render 只负责“提交更新”。
真正的 render 阶段由调度系统触发。
二十一、root.render 和 setState 的统一模型
把 root.render 和 setState 放在一起看,会更清楚。
root.render:
const update = createUpdate(lane)
update.payload = {
element: <App />
}
enqueueUpdate(hostRootFiber, update, lane)
scheduleUpdateOnFiber(root, hostRootFiber, lane)
useState 的 setState:
const update = {
lane,
action,
next: null
}
enqueueConcurrentHookUpdate(fiber, queue, update, lane)
scheduleUpdateOnFiber(root, fiber, lane)
它们的共同点是:
创建 update
分配 lane
进入 updateQueue
调度 root
不同点是:
root.render 更新的是 HostRootFiber 的 element。
setState 更新的是某个 Hook 的 state。
但最终都会走向:
scheduleUpdateOnFiber
ensureRootIsScheduled
performConcurrentWorkOnRoot
renderRoot
commitRoot
这就是 React 内部更新模型的统一性。
二十二、到这里为止,发生了什么
执行:
const root = createRoot(container)
root.render(<App />)
到目前为止,React 做了这些事。
createRoot 阶段:
创建 ReactDOMRoot
创建 FiberRootNode
创建 HostRootFiber
建立 root.current 和 hostRootFiber.stateNode 的双向关系
初始化 HostRootFiber 的 updateQueue
root.render 阶段:
拿到内部 FiberRootNode
拿到 HostRootFiber
申请本次更新的 lane
创建 update
把 ReactElement 放进 update.payload
把 update 加入 HostRootFiber.updateQueue
把 lane 标记到 root
调用 ensureRootIsScheduled 安排 root 执行
还没有发生这些事:
没有执行 App 函数
没有创建 AppFiber
没有进入 beginWork
没有创建 divFiber
没有创建真实 DOM
没有执行 commit
这就是这篇最重要的边界。
二十三、完整主线代码压缩版
可以把整条链路压缩成这样:
const root = createRoot(container)
内部:
const fiberRoot = createFiberRoot(container)
const hostRootFiber = createHostRootFiber()
fiberRoot.current = hostRootFiber
hostRootFiber.stateNode = fiberRoot
return new ReactDOMRoot(fiberRoot)
然后:
root.render(<App />)
内部:
const fiberRoot = root._internalRoot
const hostRootFiber = fiberRoot.current
const lane = requestUpdateLane(hostRootFiber)
const update = createUpdate(lane)
update.payload = {
element: <App />
}
enqueueUpdate(hostRootFiber, update, lane)
scheduleUpdateOnFiber(fiberRoot, hostRootFiber, lane)
再往后:
ensureRootIsScheduled(fiberRoot)
后面才会进入:
performConcurrentWorkOnRoot
renderRootConcurrent
workLoopConcurrent
performUnitOfWork
beginWork
所以 root.render 的位置非常明确:
它是更新链路的起点,不是 render 阶段本身。
二十四、这一篇要掌握的核心
第一,React 18 之后使用 createRoot,不是简单 API 改名,而是把 Root 这个运行时对象显式化。
第二,createRoot 主要创建 FiberRootNode 和 HostRootFiber,此时还没有用户组件 Fiber。
第三,root.render 的本质不是立即渲染 DOM,而是向 HostRootFiber 提交一次 update。
第四,ReactElement 会被放进 update.payload.element,然后进入 HostRootFiber 的 updateQueue。
第五,lane 会在创建 update 时绑定到本次更新上,后续调度和 render 都会围绕 lane 展开。
第六,scheduleUpdateOnFiber 负责把更新标记到 root,并通过 ensureRootIsScheduled 确保 root 后续被执行。
第七,到这篇为止,React 还没有进入 beginWork。beginWork 是 render 阶段处理 workInProgress Fiber 时才会发生的事情。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)