一、为什么现在入口变成了 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 内部真实模型:

先创建根容器
再向根容器提交更新

这就是 createRootroot.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 时才会发生的事情。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐