跳到主要内容

第 1 章:一切从 createRoot 开始

任何 React 19 应用,都从这两行开始:

import {createRoot} from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(<App />);

本章回答两个问题:

  1. createRoot(container) 这一行到底做了什么
  2. 为什么说它只搭舞台、不渲染

先记住这一句createRoot 给 React 盖了个舞台(FiberRoot + 根 Fiber),台上一个人没有;真正把 <App /> 搬上台的是 root.render()

一图流:createRoot 的完整调用链

逐行拆开看。

createRoot 的四件事

createRoot 的实现在 ReactDOMRoot.js:171,逻辑很直白,做了四件事。

① 校验容器container 必须是合法的 DOM 元素,否则直接抛错:

if (!isValidContainer(container)) {
throw new Error('Target container is not a DOM element.');
}

② 创建「舞台」FiberRoot:调用 createContainer,最终走到 createFiberRootnew 出一个 FiberRootNode(React 世界的"总指挥",源码在 FiberRootNode):

const root: FiberRoot = new FiberRootNode(containerInfo, tag, hydrate, ...);

FiberRootNode 上挂着 React 的"全局状态":containerInfo(我们的 DOM 容器)、pendingLanes(待处理的优先级,第 22 章主角)、current(当前渲染的 Fiber 树)……现在先把它们当成一排空抽屉,后面每章往里面塞一样东西。

③ 创建根 Fiber 并"双向绑定":舞台有了,还得有"地基"——一个代表根节点的 HostRoot FibercreateFiberRoot 里是这样配对的:

const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
root.current = uninitializedFiber; // 舞台 → 根 Fiber
uninitializedFiber.stateNode = root; // 根 Fiber → 舞台(循环引用)

root.current 指向当前 Fiber 树,fiber.stateNode 指回 root。这个"你中有我"的结构是整个 React 的起点。真正的"双缓冲"(current 树 ↔ workInProgress 树两棵树交替)是第 3 章的重点。

④ 挂事件委托listenToAllSupportedEventsReactDOMRoot.js:256)把事件监听器挂到根节点上。React 19 把所有事件都委托给根容器处理(第 19 章详解),这也是 createRoot 顺手做掉的一件大事。

最后返回一个 ReactDOMRoot 实例(ReactDOMRoot.js:259)。它只暴露两个方法:renderunmount。外部世界碰不到 FiberRoot,只能通过这两个方法跟 React 打交道。

为什么说"只搭舞台、不渲染"

注意上面没有任何一处操作 DOMcreateRoot 跑完,你页面里那个 <div id="root"></div> 还是空的

const root = createRoot(container);
// 此刻 container.innerHTML === ''

真正的渲染从 root.render() 开始。它调用 updateContainer,把 <App /> 包装成一个 update 入队,然后交给调度器(第 5 章)和工作循环(第 4 章):

const update = createUpdate(lane); // 一个待处理的更新
update.payload = {element}; // 装着 <App /> 的 payload
const root = enqueueUpdate(rootFiber, update, lane); // 入队
scheduleUpdateOnFiber(root, rootFiber, lane); // 调度 → 开工

这三个动作分别在 436455458——记住这三个名字:createUpdate / enqueueUpdate / scheduleUpdateOnFiber。它们出镜率全书最高,第 13 章(useState)和第 21 章(一次 setState 的旅程)会反复回来找它们。

一个完整的生命周期

createRoot 放进一个能跑的最小 demo(任意 Vite/CodeSandbox 项目):

import {createRoot} from 'react-dom/client';

const container = document.getElementById('root');
const root = createRoot(container); // ① 搭舞台,此刻容器是空的

root.render(<h1>第一次渲染</h1>); // ② 第一次开工
root.render(<h1>又一次渲染</h1>); // ③ 是「更新」,不是重建舞台

console.log(root._internalRoot); // FiberRootNode:舞台
console.log(root._internalRoot.current); // HostRoot Fiber:根节点

setTimeout(() => root.unmount(), 3000); // ④ 拆舞台

记住三个时间点:createRoot(搭台)→ root.render(开工)→ root.unmount(拆台)。中间的无数次 root.render 都是同一个舞台上的"换节目"。

动手实验

  1. 扒开 _internalRoot:在浏览器控制台执行上面的 demo,console.log(root._internalRoot),对照 FiberRootNode 的字段:找到 currentcontainerInfopendingLanes,逐个猜猜是干什么的(答案散落在第 4、13、22 章)。
  2. 重复 createRoot:对同一个 container 调两次 createRoot,观察控制台 warning,源码里是 warnIfReactDOMContainerInDEV 拦下的。体会一下"根只能建一次"。
  3. 断点 updateContainer:在 updateContainer 打断点,触发第一次 root.render,看调用栈和 update.payload 里是不是真的装着你的 <App />

小结

  • createRoot 只搭舞台、不渲染:创建 FiberRoot + HostRoot Fiber,双向绑定,DOM 保持原样。
  • 它顺手做了两件事:把事件委托挂到根节点、把容器标记为 React 的根。
  • root.render() 才是开工:updateContainercreateUpdateenqueueUpdatescheduleUpdateOnFiber
  • 舞台(FiberRoot)的一生:createRoot 建 → root.render 更新 → root.unmount 拆。
  • 下一步:这个舞台上的"地基",也就是 Fiber,到底是什么?翻开第 2 章。