02|从 `createRoot` 到 `scheduleUpdateOnFiber`:一次更新如何进入 React 引擎
本文解析了React从createRoot到scheduleUpdateOnFiber的更新流程,揭示了React的分层架构设计。文章首先介绍了react-dom作为平台适配层和API门面的角色,然后深入分析了createRoot如何创建FiberRoot容器、初始化事件系统,最终通过updateContainer将更新请求传递给React核心引擎。关键点包括:1) ReactDOMRoot是薄门
02|从 createRoot 到 scheduleUpdateOnFiber:一次更新如何进入 React 引擎
本栏目是「React 源码剖析」系列:我会以源码为证据、以架构为线索,讲清 React 从构建、运行时到生态边界的关键设计。开源仓库:https://github.com/facebook/react
引言:这一篇在整个 React 体系里解决什么问题?
上一章我们讲清了 React 产物的“出货方式”。这一章我们开始真正进入运行时,但仍然停在入口层:
- 你写的
createRoot(container).render(<App />) - 到底是怎么进入
react-reconciler(Fiber 引擎)的? Root这个对象究竟是什么?render调用如何变成一次“可调度的更新”?
你会看到一个非常典型的分层结构:
react-dom负责平台(DOM)适配与对外 APIreact-reconciler负责平台无关的核心引擎(Fiber/Lane/Scheduler/Commit)react-dom-bindings负责DOM 侧的细节实现(事件系统、节点映射、属性处理等)
这一章的目标不是讲完 Fiber(那会在后续章节展开),而是把“从入口到引擎”的关键交接点讲清楚。
核心概念:先对齐几组词汇
1) Facade(门面)与 Bridge(桥接)
packages/react-dom/client.js和packages/react-dom/src/client/ReactDOMClient.js是典型的 Facade:对外暴露最少的 API,把复杂性隐藏在内部。react-reconciler是一个可被多个 Renderer 复用的引擎;DOM Renderer 通过“Host Config + Bindings”把引擎桥接到真实平台。
2) Root:不是 DOM 根节点,而是 FiberRoot 的门面对象
- 你拿到的
root不是一个 class 实例(严格意义上的 ES class),而是一个函数构造器 + prototype 方法(历史兼容与编译产物友好)。 root._internalRoot才是真正的FiberRoot(来自react-reconciler)。
3) “更新”是什么?
在入口层,更新被表达为:
updateContainer(element, root, parent, callback)
它最终会:
- 选择一个
lane(优先级) - 创建一个
update - 入队
enqueueUpdate - 触发
scheduleUpdateOnFiber(进入调度/渲染循环)
源码依次解析:从 react-dom/client 到 Reconciler
Step 1:真正的入口文件只有一行
文件:packages/react-dom/client.js
export {createRoot, hydrateRoot, version} from './src/client/ReactDOMClient';
解读:
react-dom/client这个入口完全是“转发层”。- 这层存在的价值是:
- 对外 API 稳定
- 内部目录结构可自由调整
- 构建系统可以对不同入口做不同处理(上一章讲到的 bundle 矩阵)
这就是门面模式(Facade)的标准用法:把变化隔离在内部。
Step 2:ReactDOMClient 把 createRoot 导出去,同时做一堆“环境/工具”初始化
文件:packages/react-dom/src/client/ReactDOMClient.js
import {createRoot, hydrateRoot} from './ReactDOMRoot';
import {
injectIntoDevTools,
findHostInstance,
} from 'react-reconciler/src/ReactFiberReconciler';
import {canUseDOM} from 'shared/ExecutionEnvironment';
import ReactVersion from 'shared/ReactVersion';
import Internals from 'shared/ReactDOMSharedInternals';
import {ensureCorrectIsomorphicReactVersion} from '../shared/ensureCorrectIsomorphicReactVersion';
ensureCorrectIsomorphicReactVersion();
// Expose findDOMNode on internals
Internals.findDOMNode = findDOMNode;
export {ReactVersion as version, createRoot, hydrateRoot};
const foundDevTools = injectIntoDevTools();
逐块解读:
createRoot/hydrateRoot来自ReactDOMRoot,也就是说:根的创建逻辑在另一个文件里。- 这里从
react-reconciler/src/ReactFiberReconciler引入injectIntoDevTools:- 这不是“DOM 专有能力”,而是引擎层暴露给 Renderer 的能力。
- DOM 只是把它接到浏览器环境里。
ensureCorrectIsomorphicReactVersion():- 这是“工程防线”:保证
react与react-dom的版本组合正确。 - 你可以把它理解成“运行时自检”,避免用户装错版本导致不可预期行为。
- 这是“工程防线”:保证
为什么要在入口做这些?(Trade-offs)
- 好处:
- 失败更早、更明确(早发现比晚崩溃好)
- DevTools 注入是全局行为,放在入口更集中
- 代价:
- 入口文件变得“不纯”,带有 side effect
- 但对于平台级库,这是可接受的:它必须管理全局协作
Step 3:createRoot 的核心:创建容器、标记 Root、初始化事件系统
文件:packages/react-dom/src/client/ReactDOMRoot.js
3.1 Root 对象:一个薄薄的门面
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
解读:
ReactDOMRoot只保存_internalRoot。- 这是一种非常“干净”的边界:
- DOM 层不直接暴露 FiberRoot 的结构
- 外部 API 也不会被 FiberRoot 的字段变化所影响
3.2 root.render:把 children 交给 Reconciler
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
updateContainer(children, root, null, null);
};
解读(关键点只看最后一行):
updateContainer(children, root, null, null)才是“进入引擎”的动作。root.render不做 diff、不做调度、不做 DOM 操作——它只负责把“意图”交给引擎。
这是一种典型的“薄 UI 层”策略:入口层只负责参数校验与边界管理。
3.3 createRoot:把 DOM 容器变成 FiberRoot
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
if (!isValidContainer(container)) {
throw new Error('Target container is not a DOM element.');
}
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
onDefaultTransitionIndicator,
transitionCallbacks,
);
markContainerAsRoot(root.current, container);
const rootContainerElement: Document | Element | DocumentFragment =
!disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;
listenToAllSupportedEvents(rootContainerElement);
return new ReactDOMRoot(root);
}
逐块解读:
createContainer(...):- 交给
react-reconciler创建FiberRoot。 - 注意:传入的第一个参数就是
container,也就是“宿主容器”。
- 交给
markContainerAsRoot(root.current, container):- 在 DOM 节点上做标记(内部字段),用于后续判断容器是否已被 React 接管。
listenToAllSupportedEvents(rootContainerElement):- Root 初始化时就把事件系统挂上去。
- 这解释了一个常见现象:React 的事件系统是按 Root 绑定的,而不是按组件绑定的。
return new ReactDOMRoot(root):- 对外仍然返回 Facade;外界拿不到
FiberRoot。
- 对外仍然返回 Facade;外界拿不到
Trade-offs:为什么事件系统要在 createRoot 阶段就初始化?
- 好处:
- 事件委托模型要求尽早挂载(否则首次交互会漏)
- 与并发特性(例如 hydration replay)更好协同
- 代价:
- Root 创建就有全局副作用(挂事件)
- 但对于 UI 框架来说,这是典型且必要的设计
Step 4:事件系统初始化:只做一次,且覆盖所有原生事件
文件:packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
const listeningMarker = '_reactListening' + Math.random().toString(36).slice(2);
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
allNativeEvents.forEach(domEventName => {
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
const ownerDocument =
(rootContainerElement: any).nodeType === DOCUMENT_NODE
? rootContainerElement
: (rootContainerElement: any).ownerDocument;
if (ownerDocument !== null) {
if (!(ownerDocument: any)[listeningMarker]) {
(ownerDocument: any)[listeningMarker] = true;
listenToNativeEvent('selectionchange', false, ownerDocument);
}
}
}
}
逐行解读:
listeningMarker是一个随机 key:- 避免与用户字段冲突
- 避免多 Root 或重复调用时反复绑定
- 对每个
allNativeEvents:- 非
selectionchange:既绑定 bubble,也绑定 capture nonDelegatedEvents(不冒泡/特殊事件)会走另一套策略
- 非
selectionchange单独绑到 document:- 因为它不 bubble,必须挂在 document
你可以把这段代码看成 React 事件系统的“Root 级引导器”。后续我们会单独写一篇深入事件系统的插件机制(SimpleEventPlugin 等),但这章只强调一个事实:
- 事件系统是 Root 初始化的一部分
- 挂载策略是“覆盖所有事件 + 去重”
Step 5:Reconciler 接管:createContainer 如何构造 FiberRoot
文件:packages/react-reconciler/src/ReactFiberReconciler.js
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onUncaughtError: (
error: mixed,
errorInfo: {+componentStack?: ?string},
) => void,
onCaughtError: (
error: mixed,
errorInfo: {
+componentStack?: ?string,
+errorBoundary?: ?component(...props: any),
},
) => void,
onRecoverableError: (
error: mixed,
errorInfo: {+componentStack?: ?string},
) => void,
onDefaultTransitionIndicator: () => void | (() => void),
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
const hydrate = false;
const initialChildren = null;
const root = createFiberRoot(
containerInfo,
tag,
hydrate,
initialChildren,
hydrationCallbacks,
isStrictMode,
identifierPrefix,
null,
onUncaughtError,
onCaughtError,
onRecoverableError,
onDefaultTransitionIndicator,
transitionCallbacks,
);
registerDefaultIndicator(onDefaultTransitionIndicator);
return root;
}
解读:
containerInfo就是 Renderer 传进来的宿主容器(DOM 的container)。createFiberRoot是真正的“根对象构造”。- 这里并没有创建任何 Fiber 树的业务节点;它只是在创建“根架子”。
这体现了 Reconciler 的分层:
- Root 的生命周期(错误处理、标识前缀、transition callbacks)是“运行时系统配置”
- 具体渲染内容(element tree)要等到
updateContainer才会注入
Step 6:FiberRootNode:Root 里到底存了什么?
文件:packages/react-reconciler/src/ReactFiberRoot.js
function FiberRootNode(
this: $FlowFixMe,
containerInfo: any,
tag,
hydrate: any,
identifierPrefix: any,
onUncaughtError: any,
onCaughtError: any,
onRecoverableError: any,
onDefaultTransitionIndicator: any,
formState: ReactFormState<any, any> | null,
) {
this.tag = disableLegacyMode ? ConcurrentRoot : tag;
this.containerInfo = containerInfo;
this.pendingChildren = null;
this.current = null;
this.pingCache = null;
this.timeoutHandle = noTimeout;
this.cancelPendingCommit = null;
this.context = null;
this.pendingContext = null;
this.next = null;
this.callbackNode = null;
this.callbackPriority = NoLane;
this.expirationTimes = createLaneMap(NoTimestamp);
this.pendingLanes = NoLanes;
this.suspendedLanes = NoLanes;
this.pingedLanes = NoLanes;
this.warmLanes = NoLanes;
this.expiredLanes = NoLanes;
// ...(后续字段在后面章节会逐步展开)
}
解读:
containerInfo:连接宿主世界(DOM)的关键。current:Root 对应的 HostRoot Fiber(后续createFiberRoot会创建)。pendingLanes/suspendedLanes/...:并发调度的“仪表盘”(Lane 位图)。callbackNode/callbackPriority:与 Scheduler 协作的“挂起任务”记录。
你现在不需要理解所有字段,但要建立一个直觉:
- Root 是一个运行时控制面(control plane)
- 它不只是“树的根”,还是“调度、恢复、错误处理、并发状态”的聚合点
Step 7:真正的更新:updateContainer 如何把 render 变成一次调度
文件:packages/react-reconciler/src/ReactFiberReconciler.js
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?component(...props: any),
callback: ?Function,
): Lane {
const current = container.current;
const lane = requestUpdateLane(current);
updateContainerImpl(
current,
lane,
element,
container,
parentComponent,
callback,
);
return lane;
}
function updateContainerImpl(
rootFiber: Fiber,
lane: Lane,
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?component(...props: any),
callback: ?Function,
): void {
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
const update = createUpdate(lane);
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
const root = enqueueUpdate(rootFiber, update, lane);
if (root !== null) {
startUpdateTimerByLane(lane, 'root.render()', null);
scheduleUpdateOnFiber(root, rootFiber, lane);
entangleTransitions(root, rootFiber, lane);
}
}
这是本章的“交接点”:
requestUpdateLane(current):决定这次更新用哪个优先级。createUpdate(lane)+update.payload = {element}:- 更新的“内容”就是新的 element tree。
enqueueUpdate(...):把更新放进 HostRoot Fiber 的队列。scheduleUpdateOnFiber(root, rootFiber, lane):- 从这一行开始,事情就进入 WorkLoop/Scheduler 的世界。
- 也就是说:入口层到此为止,后面是引擎层的主循环。
为什么 render 不直接做工作,而要“入队 + 调度”?(Trade-offs)
- 好处:
- 可中断、可合并、可重排(并发特性依赖这一点)
- 多次
render可以被合并到同一个调度周期
- 代价:
- 调试时调用栈不再“线性直观”
- 需要 Root/UpdateQueue/Lane 这些额外抽象
React 选择了这条更难的路,是为了换取并发渲染的上限。
一图看懂:从 createRoot 到调度入口的时序
总结:这一章你应该带走的设计思想
- 入口层的职责是“校验 + 组装 + 交接”
createRoot 做的是:校验容器、创建 FiberRoot、初始化事件系统、返回门面对象。它不做渲染循环。
- Renderer 与 Reconciler 的边界非常明确
- DOM 相关:
react-dom/react-dom-bindings - 引擎相关:
react-reconciler
- “更新”被刻意设计成可调度对象(Update + Lane)
你看到的 scheduleUpdateOnFiber 是入口层的终点,也是后续 WorkLoop 的起点。
- Root 是控制面,不只是树根
从 FiberRootNode 的字段就能看出:并发、错误恢复、回调、lane 状态都围绕 Root 汇聚。
下一篇预告
第 3 篇开始我们会深入 ReactFiberWorkLoop:
scheduleUpdateOnFiber之后到底发生了什么?- WorkLoop 如何决定“渲染多少、何时 yield、何时 commit”?
- Lane 如何贯穿整个流程?
更多推荐



所有评论(0)