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 如何贯穿整个流程?
相关推荐
WeiXiao_Hyy22 分钟前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
吃杠碰小鸡38 分钟前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone44 分钟前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_09011 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农1 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king2 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳2 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵3 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星3 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_3 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js