createRoot 到底创建了什么:FiberRootNode 和 HostRootFiber 的初始化过程

一、这一篇只解决一个问题

上一篇已经讲清楚,React 18 之后入口从:

复制代码
ReactDOM.render(<App />, container)

变成了:

复制代码
const root = createRoot(container)
root.render(<App />)

这不是简单的 API 改名,而是 React 把 Root 这个运行时对象显式暴露出来了。

那么这一篇只继续往下拆一个问题:

复制代码
const root = createRoot(container)

这一行代码到底创建了什么?

也就是:

复制代码
createRoot(container)
createContainer(...)
createFiberRoot(...)
createHostRootFiber(...)
initializeUpdateQueue(...)
new ReactDOMRoot(...)

目标是把 React 应用启动时的"根结构"讲清楚。

二、先看 createRoot 阶段不做什么

很多人会把 createRootroot.render 混在一起理解。

但实际上:

复制代码
const root = createRoot(container)

执行完之后,React 还没有开始渲染你的 App。

它不会执行:

复制代码
App()

不会创建:

复制代码
AppFiber
divFiber
spanFiber

也不会创建真实 DOM 子节点。

此时 React 只是做了一件事:

为这个 DOM container 创建一个 React 内部 Root。

更准确地说,是创建一套根级别的数据结构,让后续的更新可以挂在这个 Root 上。

所以 createRoot 是初始化阶段,不是渲染阶段。

三、从用户代码开始

用户入口代码通常是:

复制代码
import { createRoot } from 'react-dom/client'

const container = document.getElementById('root')

const root = createRoot(container)

这里传入的 container 是一个真实 DOM 节点。

例如:

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

也就是说,createRoot 的参数不是 ReactElement,而是宿主环境容器。

在 React DOM 里,这个容器就是 DOM 节点。

在源码里,createRoot 首先会做一些合法性检查,例如 container 是否是合法 DOM 容器。

简化后可以理解为:

复制代码
function createRoot(container, options) {
  if (!isValidContainer(container)) {
    throw new Error('Target container is not a DOM element.')
  }

  const root = createContainer(container, ConcurrentRoot, ...)
  
  return new ReactDOMRoot(root)
}

这一段主线很清楚:

createRoot 本身不是直接创建 FiberNode 的地方。

它会继续调用 reconciler 层的 createContainer

四、createRoot 属于 react-dom,createContainer 属于 reconciler

这里要先讲一个源码分层。

createRoot 在 react-dom 里。

它关心的是 DOM 容器和对外 API。

但是 FiberRootNode、HostRootFiber 这些核心结构,是 reconciler 创建的。

也就是说:

复制代码
react-dom/client 负责入口 API
react-reconciler 负责创建 Root 和 Fiber

这个分层很重要。

React DOM 只是 React 的一个 renderer。

React Native 也可以使用 reconciler 创建自己的 Root。

所以 createRoot 会把 DOM container 传给 reconciler,让 reconciler 创建内部 root。

简化关系是:

复制代码
createRoot(container)

进入:

复制代码
createContainer(container, ConcurrentRoot, ...)

再进入:

复制代码
createFiberRoot(containerInfo, tag, ...)

这里的 containerInfo 就是外部传入的 DOM container。

五、createContainer 的核心职责

createContainer 可以理解为创建 React 内部根容器的统一入口。

简化代码:

复制代码
export function createContainer(
  containerInfo,
  tag,
  hydrationCallbacks,
  isStrictMode,
  concurrentUpdatesByDefaultOverride,
  identifierPrefix,
  onUncaughtError,
  onCaughtError,
  onRecoverableError,
  transitionCallbacks,
) {
  return createFiberRoot(
    containerInfo,
    tag,
    hydrate,
    initialChildren,
    hydrationCallbacks,
    isStrictMode,
    identifierPrefix,
    onUncaughtError,
    onCaughtError,
    onRecoverableError,
    transitionCallbacks,
  )
}

具体版本里参数会有差异,但主线是稳定的。

createContainer 本质上是薄封装。

它最重要的动作是调用:

复制代码
createFiberRoot(...)

createFiberRoot 才是真正创建 FiberRootNodeHostRootFiber 的地方。

六、ConcurrentRoot 这个参数的意义

createRoot 调用 createContainer 时,会传一个 root tag。

对于 React 18 之后的 createRoot,这个 tag 通常是:

复制代码
ConcurrentRoot

这个值非常关键。

它说明这个 Root 是并发模式 Root。

也就是说,这个 Root 后续具备并发特性,例如:

复制代码
可中断渲染
自动批处理
transition 更新
Suspense 并发行为
基于 lane 的优先级调度

如果是旧的 ReactDOM.render,对应的 Root 类型是 LegacyRoot。

所以入口 API 不只是创建方式不同,而是决定了这个 Root 的运行模式。

可以这样理解:

复制代码
createRoot(container)

创建的是:

复制代码
ConcurrentRoot

旧入口:

复制代码
ReactDOM.render(element, container)

对应的是:

复制代码
LegacyRoot

后面的调度行为、更新优先级处理、是否支持并发特性,都和这个 Root 类型有关。

所以 createRoot 阶段,React 已经确定了这个应用根的运行模式。

七、createFiberRoot:真正创建内部 Root

现在进入核心函数。

简化代码如下:

复制代码
function createFiberRoot(
  containerInfo,
  tag,
  hydrate,
  initialChildren,
  hydrationCallbacks,
  isStrictMode,
  identifierPrefix,
  onUncaughtError,
  onCaughtError,
  onRecoverableError,
  transitionCallbacks,
) {
  const root = new FiberRootNode(
    containerInfo,
    tag,
    hydrate,
    identifierPrefix,
    onUncaughtError,
    onCaughtError,
    onRecoverableError,
  )

  const uninitializedFiber = createHostRootFiber(tag, isStrictMode)

  root.current = uninitializedFiber
  uninitializedFiber.stateNode = root

  initializeUpdateQueue(uninitializedFiber)

  return root
}

这段就是 createRoot 阶段最核心的主线。

它创建了两个对象:

复制代码
FiberRootNode
HostRootFiber

然后建立它们之间的双向关系:

复制代码
root.current = uninitializedFiber
uninitializedFiber.stateNode = root

最后初始化 HostRootFiber 的 updateQueue。

这就是 createRoot 最重要的结果。

八、FiberRootNode 是什么

先看 FiberRootNode

简化结构:

复制代码
function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix) {
  this.tag = tag
  this.containerInfo = containerInfo

  this.current = null

  this.pingCache = null
  this.finishedWork = null
  this.timeoutHandle = noTimeout

  this.context = null
  this.pendingContext = null

  this.callbackNode = null
  this.callbackPriority = NoLane

  this.pendingLanes = NoLanes
  this.suspendedLanes = NoLanes
  this.pingedLanes = NoLanes
  this.expiredLanes = NoLanes
  this.finishedLanes = NoLanes
  this.errorRecoveryDisabledLanes = NoLanes

  this.entangledLanes = NoLanes
  this.entanglements = createLaneMap(NoLanes)

  this.identifierPrefix = identifierPrefix
}

字段很多,但不要逐个死记。

要按职责理解。

FiberRootNode 是 root 级状态容器。

它不代表某个组件。

它不代表某个 DOM 节点。

它代表整个 React 应用根。

它负责保存:

复制代码
宿主容器
当前 Fiber 树
待处理优先级
调度回调
render 完成后的结果
Suspense 和 hydration 相关状态
错误恢复相关状态

九、containerInfo:连接宿主环境

复制代码
this.containerInfo = containerInfo

在 React DOM 里,这个字段就是:

复制代码
document.getElementById('root')

也就是你传给 createRoot 的 DOM 容器。

它的作用是告诉 React:

最终 DOM 子节点应该挂载到哪个宿主容器里。

注意,FiberRootNode 本身不是 DOM。

它只是保存了 DOM container 的引用。

后续 commit 阶段插入 DOM 时,需要通过 HostRootFiber 找到 FiberRootNode,再通过 FiberRootNode 找到 containerInfo。

也就是说,这条链路是:

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

这就是 Root 和真实 DOM 容器之间的连接方式。

十、current:当前已经生效的 Fiber 树

复制代码
this.current = null

创建完 HostRootFiber 后,会赋值:

复制代码
root.current = uninitializedFiber

这里的 current 是 React 双缓存模型里的 current tree 根节点。

它表示:

当前已经提交、生效在页面上的 Fiber 树。

在刚执行 createRoot 时,页面上还没有 React 渲染出来的内容。

但 React 仍然需要一个根 Fiber。

这个根 Fiber 就是 HostRootFiber。

所以此时:

复制代码
fiberRoot.current = hostRootFiber

注意,这个 current 指向的是 Fiber 节点,不是 DOM。

FiberRootNode 是 root 管理对象。

HostRootFiber 才是 Fiber 树的根节点。

十一、pendingLanes:Root 上有哪些待处理更新

复制代码
this.pendingLanes = NoLanes

刚 createRoot 时,pendingLanes 是空的。

因为还没有调用:

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

也没有任何 setState。

当后续调用 root.render 或组件 setState 时,React 会给更新分配 lane,并把 lane 合并到 root.pendingLanes 上。

例如可以抽象理解为:

复制代码
root.pendingLanes |= lane

这个字段告诉 React:

当前 root 上有哪些优先级的更新需要处理。

调度阶段会根据它选择下一批要处理的 lanes。

这也说明一个关键点:

调度不是直接看某个组件。

调度最终是看 Root。

组件上的更新会通过 Fiber 树冒泡到 Root,然后 Root 根据 pendingLanes 安排任务。

十二、callbackNode 和 callbackPriority:Root 的调度回调

复制代码
this.callbackNode = null
this.callbackPriority = NoLane

这两个字段和调度有关。

当 Root 上有更新时,React 会通过 ensureRootIsScheduled 为这个 Root 安排任务。

如果已经有一个任务被安排了,React 不一定要重复安排。

它会比较当前已有 callback 的优先级和新的最高优先级。

例如:

复制代码
if (existingCallbackPriority === newCallbackPriority) {
  return
}

这就需要 root 保存:

复制代码
当前已经安排的 callback 是什么
当前 callback 的优先级是什么

所以这两个字段是 Root 级别调度复用的基础。

刚 createRoot 时,它们为空,因为还没有任何更新。

十三、finishedWork:render 阶段完成后的 Fiber 树

复制代码
this.finishedWork = null

后续 render 阶段完成后,React 会得到一棵完整的 workInProgress 树。

这棵树暂时挂在:

复制代码
root.finishedWork

然后 commit 阶段会提交它。

commit 完成后,React 会把:

复制代码
root.current

切换到这棵新的 finishedWork。

这个过程就是双缓存切换。

刚 createRoot 时,还没有 render,所以 finishedWork 是 null。

但是字段已经初始化好了。

十四、为什么 FiberRootNode 不是 Fiber

这里必须把概念区分清楚。

FiberRootNode 不是 Fiber。

它没有:

复制代码
return
child
sibling
tag
pendingProps
memoizedProps
flags

这些 Fiber 节点字段。

它是整个 root 的管理对象。

真正的 Fiber 树根节点是 HostRootFiber。

所以不要把:

复制代码
FiberRootNode
HostRootFiber

混成一个东西。

可以这样理解:

复制代码
FiberRootNode 管理整个应用根
HostRootFiber 是 Fiber 树的根节点

它们之间通过两个字段互相连接:

复制代码
fiberRoot.current = hostRootFiber
hostRootFiber.stateNode = fiberRoot

这组关系后面会频繁出现。

十五、createHostRootFiber:创建 Fiber 树的根节点

现在看第二个核心对象。

复制代码
const uninitializedFiber = createHostRootFiber(tag, isStrictMode)

createHostRootFiber 会创建一个 tag 为 HostRoot 的 Fiber。

简化逻辑:

复制代码
function createHostRootFiber(tag, isStrictMode) {
  let mode

  if (tag === ConcurrentRoot) {
    mode = ConcurrentMode
    if (isStrictMode === true) {
      mode |= StrictLegacyMode | StrictEffectsMode
    }
  } else {
    mode = NoMode
  }

  return createFiber(HostRoot, null, null, mode)
}

这段代码里有几个重点。

第一,HostRootFiber 的 tag 是 HostRoot。

第二,Root 的类型会影响 Fiber 的 mode。

第三,StrictMode 也会影响 mode。

第四,最终通过 createFiber 创建 FiberNode。

十六、HostRootFiber 的 tag

复制代码
createFiber(HostRoot, null, null, mode)

这里的第一个参数是:

复制代码
HostRoot

这说明这个 Fiber 是一棵 Fiber 树的根。

它不是用户写的组件。

不是 App。

不是 div。

它是 React 内部根 Fiber。

后续 render 阶段第一次 beginWork,处理的就是这个 HostRootFiber。

也就是说,React 的 Fiber 遍历不是从 AppFiber 开始的。

而是从 HostRootFiber 开始。

第一次用户传入的 <App />,后面会作为 HostRootFiber 的子节点被调和出来。

十七、HostRootFiber 的 pendingProps 为什么是 null

创建 HostRootFiber 时:

复制代码
createFiber(HostRoot, null, null, mode)

第二个参数是 pendingProps。

这里是 null。

为什么?

因为 HostRootFiber 不是用户组件,它没有 JSX props。

它的数据来源不是 props,而是 updateQueue。

后续:

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

传进来的 <App /> 会被包装成 update,放进 HostRootFiber.updateQueue。

render 阶段处理 HostRootFiber 时,会通过 processUpdateQueue 得到:

复制代码
memoizedState.element

再拿这个 element 去调和子节点。

所以 HostRootFiber 和普通组件 Fiber 不一样。

普通 FunctionComponent 的输入来自 pendingProps。

HostRootFiber 的核心输入来自 updateQueue。

这是后面理解 updateHostRoot 的关键。

十八、HostRootFiber 的 stateNode 指回 FiberRootNode

创建完两个对象后,源码会建立连接:

复制代码
root.current = uninitializedFiber
uninitializedFiber.stateNode = root

这两个赋值要一起看。

复制代码
root.current = uninitializedFiber

表示:

FiberRootNode 当前生效的 Fiber 树根节点是 HostRootFiber。

复制代码
uninitializedFiber.stateNode = root

表示:

HostRootFiber 对应的实例是 FiberRootNode。

为什么 HostRootFiber.stateNode 不是 DOM?

因为 HostRootFiber 对应的是 React Root,不是某个 DOM 元素。

对于不同 Fiber,stateNode 含义不同:

复制代码
HostRootFiber.stateNode 是 FiberRootNode
HostComponentFiber.stateNode 是 DOM 节点
ClassComponentFiber.stateNode 是 class 实例
FunctionComponentFiber.stateNode 是 null

所以 stateNode 不能简单理解成 DOM。

它表示这个 Fiber 对应的运行时实例。

十九、initializeUpdateQueue:为什么 createRoot 就要初始化更新队列

createFiberRoot 里还有一个重要动作:

复制代码
initializeUpdateQueue(uninitializedFiber)

也就是给 HostRootFiber 初始化 updateQueue。

简化结构:

复制代码
function initializeUpdateQueue(fiber) {
  const queue = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
      pending: null
    },
    callbacks: null
  }

  fiber.updateQueue = queue
}

这一步非常关键。

因为下一步用户调用:

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

React 会创建一个 update,然后把它放进 HostRootFiber.updateQueue。

如果 createRoot 阶段不初始化 updateQueue,root.render 阶段就没有地方存 update。

所以 createRoot 不只是创建 root,还提前为 root 更新准备好了队列。

二十、HostRootFiber 的 memoizedState

在 HostRootFiber 上,memoizedState 通常会保存 root 级别状态。

对于 HostRoot 来说,最关键的是 element。

可以简化理解为:

复制代码
hostRootFiber.memoizedState = {
  element: null
}

刚 createRoot 时,element 是 null。

因为还没有 render 任何东西。

当执行:

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

之后,updateQueue 会被塞入一个 update:

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

render 阶段处理 HostRootFiber 的 updateQueue 后,memoizedState 会变成:

复制代码
hostRootFiber.memoizedState = {
  element: <App />
}

然后 React 才会拿这个 element 去构建子 Fiber。

这说明 <App /> 不是 createRoot 阶段出现的。

它是在 root.render 之后才进入 HostRootFiber 的 updateQueue。

二十一、createRoot 结束后内部结构长什么样

执行完:

复制代码
const root = createRoot(container)

后,内部大致是:

复制代码
ReactDOMRoot {
  _internalRoot: FiberRootNode
}

FiberRootNode:

复制代码
{
  tag: ConcurrentRoot,
  containerInfo: container,
  current: HostRootFiber,
  pendingLanes: NoLanes,
  callbackNode: null,
  callbackPriority: NoLane,
  finishedWork: null
}

HostRootFiber:

复制代码
{
  tag: HostRoot,
  stateNode: FiberRootNode,
  return: null,
  child: null,
  sibling: null,
  pendingProps: null,
  memoizedState: {
    element: null
  },
  updateQueue: initializedQueue,
  lanes: NoLanes,
  childLanes: NoLanes,
  flags: NoFlags,
  alternate: null
}

此时最重要的状态是:

复制代码
hostRootFiber.child === null

说明用户组件树还没出现。

复制代码
hostRootFiber.updateQueue 已经存在

说明它已经准备好接收 root.render 的 update。

复制代码
fiberRoot.pendingLanes === NoLanes

说明当前 root 还没有待处理更新。

复制代码
fiberRoot.callbackNode === null

说明当前 root 还没有被调度。

这就是 createRoot 完成后的准确状态。

二十二、为什么 createRoot 不直接创建 workInProgress

createRoot 阶段只创建 current 树的根节点。

它不会创建 workInProgress 树。

workInProgress 是在真正进入 render 阶段时创建的。

原因是:

workInProgress 代表"一次正在进行的渲染工作"。

而 createRoot 只是创建根容器,还没有任何更新。

没有 update,就没有 render。

没有 render,就没有 workInProgress。

后续 root.render 之后,React 会调度 root。

等真正开始 render 时,才会执行类似:

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

基于 current HostRootFiber 创建 workInProgress HostRootFiber。

所以 createRoot 阶段只负责创建当前树的基础根节点。

它不负责创建本次渲染工作树。

二十三、为什么 createRoot 必须先创建 HostRootFiber

这个问题很重要。

如果还没有 App,为什么需要一个 HostRootFiber?

因为 root.render 本身就是一次更新。

而 update 必须挂在某个 Fiber 上。

第一次 render 时,AppFiber 还不存在。

所以只能先有一个内部根 Fiber 来承载这次更新。

这个 Fiber 就是 HostRootFiber。

调用:

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

实际上会创建一个 update,并挂到:

复制代码
hostRootFiber.updateQueue

如果没有 HostRootFiber,首次渲染的 update 就没有挂载位置。

所以 HostRootFiber 是整个更新系统的启动点。

它不是为了显示 UI。

它是为了让 root 级别更新进入 Fiber 更新队列。

二十四、createRoot 和后续 root.render 的边界

现在可以明确边界。

createRoot 做了这些事:

复制代码
校验 container
创建 FiberRootNode
创建 HostRootFiber
连接 FiberRootNode 和 HostRootFiber
初始化 HostRootFiber.updateQueue
返回 ReactDOMRoot

createRoot 没有做这些事:

复制代码
没有接收 ReactElement
没有创建 update
没有分配 lane
没有调度 root
没有执行组件函数
没有创建 AppFiber
没有操作 DOM 子节点

下一步 root.render 才会做:

复制代码
接收 ReactElement
创建 update
把 element 放进 update.payload
把 update 放进 HostRootFiber.updateQueue
分配 lane
调度 root

这两个阶段必须分开理解。

二十五、这一篇的核心结论

createRoot(container) 的本质不是渲染,而是初始化 React Root。

它创建了用户层的 ReactDOMRoot,内部层的 FiberRootNode,以及 Fiber 树的根节点 HostRootFiber

FiberRootNode 负责管理整个 root 的状态,包括 container、current、pendingLanes、callbackNode、finishedWork 等。

HostRootFiber 是 Fiber 树的根节点,它不是 App,也不是 DOM,而是 root 级更新的承载点。

两者通过下面关系连接:

复制代码
fiberRoot.current = hostRootFiber
hostRootFiber.stateNode = fiberRoot

同时,React 会初始化 HostRootFiber 的 updateQueue,为后续 root.render(<App />) 提交 update 做准备。

所以执行完:

复制代码
const root = createRoot(container)

之后,React 内部已经有了 root 结构,但还没有用户组件树。

相关推荐
习明然2 小时前
UniApp开发体验感受总结
前端·uni-app
刀法如飞2 小时前
Claude Code Skills 推荐:2026年最值得安装的10个AI技能
前端·后端·ai编程
阿豪只会阿巴3 小时前
【没事学点啥】TurboBlog轻量级个人博客项目——项目介绍
javascript·python·django·html
Lee川3 小时前
面试手写 KeepAlive:React 组件缓存的实现原理
前端·react.js·面试
墨染天姬3 小时前
【AI】cursor提示词小技巧
前端·数据库·人工智能
烛阴3 小时前
TEngine 入门系列(一):TEngine 是什么 & 为什么选它
前端·unity3d
转转技术团队3 小时前
WebNN:让 AI 推理在浏览器中“零距离”运行
前端
刀法如飞4 小时前
TypeScript 数组去重的 20 种实现方式,哪一种你还不知道?
前端·javascript·算法
IT_陈寒4 小时前
Vite热更新失效?你可能漏了这个小细节
前端·人工智能·后端