为什么 React 18 之后使用 createRoot,而不是 ReactDOM.render

一、入口 API 的变化不是语法变化,而是模型变化

在 React 18 之前,应用入口通常这样写:

复制代码
import ReactDOM from 'react-dom'

ReactDOM.render(<App />, document.getElementById('root'))

从 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 把"Root"这个运行时概念显式暴露了出来。

旧 API 的表达重点是:

复制代码
把 element 渲染到 container

新 API 的表达重点是:

复制代码
先为 container 创建一个 Root,再向这个 Root 提交更新

这两个模型的差异很大。

因为 React 18 之后,React 不再把渲染理解成一次孤立的同步过程,而是把它理解成:

复制代码
某个 Root 上发生了一次带优先级的更新,React 需要根据调度策略决定什么时候处理它

所以 createRoot 的出现,本质上是为了把 React 内部早就存在的 Root 模型暴露到用户 API 层。

二、ReactDOM.render 的语义为什么不够准确

旧 API:

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

这个 API 容易给人一个直觉:

调用 render,React 就开始把 <App /> 渲染成 DOM。

这在早期同步渲染模型下问题不明显,因为 React 的更新基本就是同步递归执行。

但是到了 React 18 的并发模型,这个语义就不够准确了。

因为一次更新不一定马上完成。

它可能会:

复制代码
被赋予不同优先级
被批量合并
被中断
被恢复
被更高优先级任务插队
因为 Suspense 挂起
在之后某个时间继续执行

这些能力都要求 React 必须长期维护一个 root 级别的运行时对象。

这个 root 对象不能只是一个 DOM container。

它需要保存很多和更新系统相关的信息。

比如:

复制代码
当前已经提交的 Fiber 树
正在构建中的 Fiber 树
当前 root 上待处理的 lanes
当前 root 是否已经安排调度任务
当前 root 的调度回调是什么
render 完成后等待 commit 的 finishedWork
Suspense 挂起相关状态
hydration 相关状态
错误恢复状态

这些信息都属于"Root 级别状态"。

所以旧 API 最大的问题不是功能不足,而是语义上把 Root 隐藏了。

它看起来像:

复制代码
ReactDOM.render 是一次性动作

但实际内部需要的是:

复制代码
Root 是一个长期存在的运行时容器

React 18 之后把 API 改成 createRoot().render(),就是让外部使用方式更接近内部模型。

三、createRoot 显式表达了一个核心事实:Root 会长期存在

新写法:

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

这里的 root 不是一次性的临时对象。

它表示这个 container 对应的 React 根。

后续所有对这个根的操作,都应该通过这个 root 对象完成。

例如:

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

root.render(<OtherApp />)

root.unmount()

也就是说,root 是一个长期对象。

它背后关联着 React 内部的根容器。

你可以把这个模型理解成:

复制代码
DOM container 是宿主环境的根
React Root 是 React 更新系统的根

它们不是同一个东西。

DOM container 只是告诉 React:

复制代码
最终 DOM 要挂载到哪里

React Root 负责管理:

复制代码
这个应用的更新、调度、Fiber 树和提交结果

所以 createRoot(container) 的真正意义不是"准备渲染",而是:

复制代码
把一个 DOM 容器注册成 React 可以管理的 Root

四、旧入口把两个阶段合在了一起

旧写法:

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

把两个概念混在了一起:

复制代码
创建 Root
提交更新

新写法把它们拆开:

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

第一步:

复制代码
createRoot(container)

负责创建 Root。

第二步:

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

负责向 Root 提交一次更新。

这个拆分非常关键。

因为从 React 内部看,这两件事本来就不是一件事。

创建 Root 时,React 要初始化 root 级别的数据结构。

提交更新时,React 要创建 update、分配 lane、进入调度流程。

这两个阶段职责完全不同。

如果 API 继续使用 ReactDOM.render(element, container),外部看起来就像"传入 element 就马上渲染"。

但真实内部并不是这样。

真实内部更接近:

复制代码
container 先被创建成 React Root
element 再作为一次更新提交到这个 Root 上

五、createRoot 背后真正创建的是 React 的 Root 容器

用户代码里:

复制代码
const root = createRoot(container)

拿到的是一个用户层对象。

在源码里,它通常对应一个 ReactDOMRoot 实例。

简化结构可以理解成:

复制代码
function ReactDOMRoot(internalRoot) {
  this._internalRoot = internalRoot
}

也就是说,用户拿到的 root 只是一个外壳。

真正保存 React 内部状态的是:

复制代码
root._internalRoot

这个内部 root 就是 React 的 FiberRootNode

可以这样分层:

复制代码
ReactDOMRoot:暴露给用户的 API 对象
FiberRootNode:React 内部管理更新的根容器
DOM container:真实宿主环境容器

这三个东西要分清楚。

container 是 DOM 节点。

ReactDOMRoot 是用户操作入口。

FiberRootNode 是 React 更新系统真正的根。

六、为什么 Root 必须独立于 DOM container

有一个容易误解的点:

既然 React 最终是把内容渲染到 container,为什么不直接把所有状态挂到 container 上?

原因是 DOM container 只属于宿主环境,它不是 React 的运行时对象。

React 需要保存的很多东西和 DOM 本身无关。

例如:

复制代码
pendingLanes
suspendedLanes
pingedLanes
expiredLanes
callbackNode
callbackPriority
finishedWork
current

这些都是 React 调度和 Fiber 架构的内部状态。

它们不应该直接挂在 DOM 节点上。

更重要的是,React 不只支持 DOM。

React reconciler 的设计本身是跨宿主环境的。

React DOM 有 container。

React Native 有自己的宿主容器。

其他 renderer 也可以有不同的宿主环境。

所以 React 需要一个抽象的 Root 对象来承载框架内部状态,而不是把 Root 和某个具体 DOM 节点绑死。

这就是 FiberRootNode 的意义。

七、createRoot 对应的是 Concurrent Root

React 18 的 createRoot 创建的是 Concurrent Root。

旧的 ReactDOM.render 对应的是 Legacy Root。

这不是命名差异,而是运行模式差异。

Legacy Root 更接近旧的同步渲染模型。

Concurrent Root 支持 React 18 的并发能力。

例如:

复制代码
自动批处理
startTransition
Suspense 并发行为
可中断渲染
更细粒度的优先级调度

所以 React 18 里推荐使用:

复制代码
createRoot(container)

不是因为新 API 更好看,而是因为它让应用进入新的 Root 模式。

可以理解为:

复制代码
ReactDOM.render 创建的是旧模式 Root
createRoot 创建的是并发模式 Root

Root 的 tag 不同,后续调度和 render 行为就会不同。

也就是说,入口 API 决定了这个应用根节点使用哪套运行模式。

这也是为什么 React 18 升级时,很多新能力要求你从 ReactDOM.render 迁移到 createRoot

八、为什么 root.render 不是 createRoot 的一部分

有人可能会问:

既然 createRoot 之后基本都会 render,为什么不直接:

复制代码
createRoot(container, <App />)

React 没这样设计,是因为创建 Root 和提交更新是两个不同阶段。

Root 可以先创建,但不一定马上渲染。

例如:

复制代码
const root = createRoot(container)

此时 React 只初始化内部根结构。

什么时候提交 UI,由你调用:

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

而且同一个 root 可以多次 render:

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

root.render(<App theme="dark" />)

root.render(<OtherApp />)

这些都是对同一个 Root 提交不同更新。

如果把创建 Root 和首次 render 强绑在一起,就无法清晰表达:

复制代码
Root 是长期存在的
render 是发生在 Root 上的一次更新

React 现在的 API 正是在强调这个边界。

九、root.render 的语义也不是"执行渲染"

虽然名字叫 render,但从内部模型看,root.render(element) 更准确的语义是:

复制代码
向 Root 提交一个 element 更新

它不是立即执行组件函数。

不是立即创建 Fiber 子树。

不是立即操作 DOM。

它只是把你传入的 element 作为更新内容交给 React。

也就是说:

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

更接近:

复制代码
root.update({
  element: <App />
})

当然,React API 不会这么命名,因为从用户视角看确实是在"渲染应用"。

但是写源码专栏时,要把语义分清楚。

用户语义:

复制代码
render App

源码语义:

复制代码
submit root update

这两个不是一个层级。

十、从 API 变化看 React 的架构变化

从:

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

到:

复制代码
const root = createRoot(container)
root.render(element)

React 实际上传达了几个架构变化。

第一,Root 是显式对象。

这说明 React 需要长期维护 root 级状态。

第二,render 是 Root 上的一次更新。

这说明 React 把首次渲染和后续更新统一进 update 模型。

第三,Root 决定运行模式。

createRoot 创建 Concurrent Root,旧 API 创建 Legacy Root。

第四,调度以 Root 为单位。

React 不是孤立处理某个组件,而是从 root 级别选择 lanes 并安排任务。

第五,Fiber 树挂在 Root 下面。

Root 持有当前 Fiber 树,render 阶段构建 workInProgress 树,commit 后切换 current。

相关推荐
WindrunnerMax2 小时前
基于 Markdown-It 的无序列表折叠插件
前端·javascript·github
剑神一笑2 小时前
CSS Loading 动画生成器
前端·css
神三元2 小时前
最近半年,我做了个 AI-Native 的 Agent 从零到进阶教程
前端·javascript·面试
XiYang-DING2 小时前
jQuery
前端·javascript·jquery
Morwit2 小时前
【力扣hot100】 221. 最大正方形
前端·算法·leetcode
明月_清风2 小时前
关于node 模块化的现状与未来
前端·node.js
老王以为2 小时前
从源码到架构:React useActionState 深度剖析
前端·javascript·react.js
前端超有趣2 小时前
详解JavaScript中encodeURIComponent和decodeURIComponent的使用(附实战场景)
前端·javascript
XinZong3 小时前
业余抱团搞副业:基于OpenClaw做了一款AI社交虾聊,产品做完了,求运营思路
javascript