02|从 `createRoot` 到 `scheduleUpdateOnFiber`:一次更新如何进入 React 引擎

02|从 createRootscheduleUpdateOnFiber:一次更新如何进入 React 引擎

本栏目是「React 源码剖析」系列:我会以源码为证据、以架构为线索,讲清 React 从构建、运行时到生态边界的关键设计。开源仓库:https://github.com/facebook/react

引言:这一篇在整个 React 体系里解决什么问题?

上一章我们讲清了 React 产物的"出货方式"。这一章我们开始真正进入运行时,但仍然停在入口层

  • 你写的 createRoot(container).render(<App />)
  • 到底是怎么进入 react-reconciler(Fiber 引擎)的?
  • Root 这个对象究竟是什么?
  • render 调用如何变成一次"可调度的更新"?

你会看到一个非常典型的分层结构:

  • react-dom 负责平台(DOM)适配对外 API
  • react-reconciler 负责平台无关的核心引擎(Fiber/Lane/Scheduler/Commit)
  • react-dom-bindings 负责DOM 侧的细节实现(事件系统、节点映射、属性处理等)

这一章的目标不是讲完 Fiber(那会在后续章节展开),而是把"从入口到引擎"的关键交接点讲清楚。


核心概念:先对齐几组词汇

1) Facade(门面)与 Bridge(桥接)

  • packages/react-dom/client.jspackages/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

js 复制代码
export {createRoot, hydrateRoot, version} from './src/client/ReactDOMClient';

解读:

  • react-dom/client 这个入口完全是"转发层"。
  • 这层存在的价值是:
    • 对外 API 稳定
    • 内部目录结构可自由调整
    • 构建系统可以对不同入口做不同处理(上一章讲到的 bundle 矩阵)

这就是门面模式(Facade)的标准用法:把变化隔离在内部


Step 2:ReactDOMClientcreateRoot 导出去,同时做一堆"环境/工具"初始化

文件:packages/react-dom/src/client/ReactDOMClient.js

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();
js 复制代码
// 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()
    • 这是"工程防线":保证 reactreact-dom 的版本组合正确。
    • 你可以把它理解成"运行时自检",避免用户装错版本导致不可预期行为。

为什么要在入口做这些?(Trade-offs)

  • 好处:
    • 失败更早、更明确(早发现比晚崩溃好)
    • DevTools 注入是全局行为,放在入口更集中
  • 代价:
    • 入口文件变得"不纯",带有 side effect
    • 但对于平台级库,这是可接受的:它必须管理全局协作

Step 3:createRoot 的核心:创建容器、标记 Root、初始化事件系统

文件:packages/react-dom/src/client/ReactDOMRoot.js

3.1 Root 对象:一个薄薄的门面
js 复制代码
function ReactDOMRoot(internalRoot: FiberRoot) {
  this._internalRoot = internalRoot;
}

解读:

  • ReactDOMRoot 只保存 _internalRoot
  • 这是一种非常"干净"的边界:
    • DOM 层不直接暴露 FiberRoot 的结构
    • 外部 API 也不会被 FiberRoot 的字段变化所影响
3.2 root.render:把 children 交给 Reconciler
js 复制代码
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
js 复制代码
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

Trade-offs:为什么事件系统要在 createRoot 阶段就初始化?

  • 好处:
    • 事件委托模型要求尽早挂载(否则首次交互会漏)
    • 与并发特性(例如 hydration replay)更好协同
  • 代价:
    • Root 创建就有全局副作用(挂事件)
    • 但对于 UI 框架来说,这是典型且必要的设计

Step 4:事件系统初始化:只做一次,且覆盖所有原生事件

文件:packages/react-dom-bindings/src/events/DOMPluginEventSystem.js

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

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

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

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;
}
js 复制代码
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 到调度入口的时序

ReactFiberRoot ReactFiberReconciler DOMPluginEventSystem ReactDOMRoot ReactDOMClient packages/react-dom/client.js 用户代码 ReactFiberRoot ReactFiberReconciler DOMPluginEventSystem ReactDOMRoot ReactDOMClient packages/react-dom/client.js 用户代码 createRoot(container) export createRoot createRoot(container, options) createContainer(container, ConcurrentRoot, ...) createFiberRoot(containerInfo, tag, ...) markContainerAsRoot(root.current, container) listenToAllSupportedEvents(rootContainerElement) return new ReactDOMRoot(_internalRoot) root.render(<App />) updateContainer(children, _internalRoot, null, null) requestUpdateLane(current) createUpdate(lane) + enqueueUpdate(...) scheduleUpdateOnFiber(root, rootFiber, lane)


总结:这一章你应该带走的设计思想

  1. 入口层的职责是"校验 + 组装 + 交接"

createRoot 做的是:校验容器、创建 FiberRoot、初始化事件系统、返回门面对象。它不做渲染循环。

  1. Renderer 与 Reconciler 的边界非常明确
  • DOM 相关:react-dom / react-dom-bindings
  • 引擎相关:react-reconciler
  1. "更新"被刻意设计成可调度对象(Update + Lane)

你看到的 scheduleUpdateOnFiber 是入口层的终点,也是后续 WorkLoop 的起点。

  1. Root 是控制面,不只是树根

FiberRootNode 的字段就能看出:并发、错误恢复、回调、lane 状态都围绕 Root 汇聚。


下一篇预告

第 3 篇开始我们会深入 ReactFiberWorkLoop

  • scheduleUpdateOnFiber 之后到底发生了什么?
  • WorkLoop 如何决定"渲染多少、何时 yield、何时 commit"?
  • Lane 如何贯穿整个流程?
相关推荐
Electrolux几秒前
[wllama]纯前端实现大语言模型调用:在浏览器里跑 AI 是什么体验。以调用腾讯 HY-MT1.5 混元翻译模型为例
前端·aigc·ai编程
sanra1234 分钟前
前端定位相关技巧
前端·vue
起名时在学Aiifox7 分钟前
从零实现前端数据格式化工具:以船员经验数据展示为例
前端·vue.js·typescript·es6
cute_ming12 分钟前
关于基于nodeMap重构DOM的最佳实践
java·javascript·重构
oMcLin25 分钟前
如何在Manjaro Linux上配置并优化Caddy Web服务器,确保高并发流量下的稳定性与安全性?
linux·服务器·前端
码途潇潇35 分钟前
JavaScript 中 ==、===、Object.is 以及 null、undefined、undeclared 的区别
前端·javascript
之恒君38 分钟前
Node.js 模块加载 - 4 - CJS 和 ESM 互操作避坑清单
前端·node.js
be or not to be1 小时前
CSS 背景(background)系列属性
前端·css·css3
前端snow1 小时前
在手机端做个滚动效果
前端
webkubor1 小时前
🧠 2025:AI 写代码越来越强,但我的项目返工却更多了
前端·机器学习·ai编程