第 1 章:一切从 createRoot 开始
任何 React 19 应用,都从这两行开始:
import {createRoot} from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
本章回答两个问题:
createRoot(container)这一行到底做了什么?- 为什么说它只搭舞台、不渲染?
先记住这一句:
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,最终走到 createFiberRoot,new 出一个 FiberRootNode(React 世界的"总指挥",源码在 FiberRootNode):
const root: FiberRoot = new FiberRootNode(containerInfo, tag, hydrate, ...);
FiberRootNode 上挂着 React 的"全局状态":containerInfo(我们的 DOM 容器)、pendingLanes(待处理的优先级,第 22 章主角)、current(当前渲染的 Fiber 树)……现在先把它们当成一排空抽屉,后面每章往里面塞一样东西。
③ 创建根 Fiber 并"双向绑定":舞台有了,还得有"地基"——一个代表根节点的 HostRoot Fiber。createFiberRoot 里是这样配对的:
const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
root.current = uninitializedFiber; // 舞台 → 根 Fiber
uninitializedFiber.stateNode = root; // 根 Fiber → 舞台(循环引用)
root.current 指向当前 Fiber 树,fiber.stateNode 指回 root。这个"你中有我"的结构是整个 React 的起点。真正的"双缓冲"(current 树 ↔ workInProgress 树两棵树交替)是第 3 章的重点。
④ 挂事件委托:listenToAllSupportedEvents(ReactDOMRoot.js:256)把事件监听器挂到根节点上。React 19 把所有事件都委托给根容器处理(第 19 章详解),这也是 createRoot 顺手做掉的一件大事。
最后返回一个 ReactDOMRoot 实例(ReactDOMRoot.js:259)。它只暴露两个方法:render 和 unmount。外部世界碰不到 FiberRoot,只能通过这两个方法跟 React 打交道。
为什么说"只搭舞台、不渲染"
注意上面没有任何一处操作 DOM。createRoot 跑完,你页面里那个 <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); // 调度 → 开工
这三个动作分别在 436、455、458——记住这三个名字: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 都是同一个舞台上的"换节目"。
动手实验
- 扒开
_internalRoot:在浏览器控制台执行上面的 demo,console.log(root._internalRoot),对照 FiberRootNode 的字段:找到current、containerInfo、pendingLanes,逐个猜猜是干什么的(答案散落在第 4、13、22 章)。 - 重复
createRoot:对同一个container调两次createRoot,观察控制台 warning,源码里是 warnIfReactDOMContainerInDEV 拦下的。体会一下"根只能建一次"。 - 断点
updateContainer:在 updateContainer 打断点,触发第一次root.render,看调用栈和update.payload里是不是真的装着你的<App />。
小结
createRoot只搭舞台、不渲染:创建 FiberRoot + HostRoot Fiber,双向绑定,DOM 保持原样。- 它顺手做了两件事:把事件委托挂到根节点、把容器标记为 React 的根。
root.render()才是开工:updateContainer→createUpdate→enqueueUpdate→scheduleUpdateOnFiber。- 舞台(FiberRoot)的一生:
createRoot建 →root.render更新 →root.unmount拆。 - 下一步:这个舞台上的"地基",也就是 Fiber,到底是什么?翻开第 2 章。