Read OSS

架构概览:读懂 React 源码的地图

中级

前置知识

  • 具备 JavaScript 基础及模块系统知识(import/export)
  • 有 React 使用经验(组件、JSX、Hooks)
  • 对构建工具有基本了解(打包器、模块解析)

架构概览:读懂 React 源码的地图

初次接触 React 源码,很多人会感到无从下手——并非因为某个文件特别复杂,而是因为整个代码库由约 38 个包组成,这些包通过一套自定义构建流水线连接在一起,并在编译阶段完成模块替换。在深入理解 reconciliation 的工作原理或 Hooks 的状态存储机制之前,你首先需要建立一张清晰的全局地图:各部分在哪里、它们如何协作。本文的目的正在于此。

我们将依次走过 monorepo 的目录结构、梳理包之间的依赖关系,最后重点剖析 React 最具特色的架构模式——fork 系统。这套机制在构建阶段完成整个模块的替换,从而为开源版本、Meta 内部 Web 应用(FB_WWW)和 React Native 分别生成对应的产物。

Monorepo 结构与各包的职责

React 采用 Yarn workspaces monorepo 管理。根目录的 package.json 通过一条 glob 声明了所有工作区:

{
  "private": true,
  "workspaces": ["packages/*"]
}

所有可发布的包都位于 packages/ 下。版本号统一在 ReactVersions.js 中管理,该文件是版本的唯一数据来源——当前为 19.3.0,并维护了每个包名到版本号的映射,从而避免 monorepo 内出现版本漂移。

目录 职责
packages/react 公共 API 层——createElement、Hooks 桩、组件类型定义
packages/react-reconciler Fiber reconciler——所有渲染器共用的核心引擎
packages/react-dom DOM 渲染器入口(createRoothydrateRoot
packages/react-dom-bindings DOM 专属的 host config、事件系统与属性处理
packages/react-server 服务端渲染引擎——Fizz(SSR)和 Flight(RSC)
packages/react-client Flight 客户端——负责反序列化 RSC 数据
packages/scheduler 基于优先级队列的协作式调度
packages/shared 跨包通用工具、feature flags、共享类型
packages/react-native-renderer React Native 的 host config 与渲染器
compiler/ React Compiler(原 React Forget)——独立的构建系统

compiler/ 是一个完全独立的子项目,拥有自己的 package.json 和构建流水线,不属于 Yarn workspaces 的管理范围。

包依赖关系图

React 的包架构严格遵循分层设计,这也是它能够支持多渲染器的根本原因。理清这张依赖图,是读懂源码的关键第一步。

graph TD
    react["react<br/>(Public API)"]
    shared["shared<br/>(Utilities, Types, Feature Flags)"]
    reconciler["react-reconciler<br/>(Fiber Engine)"]
    dom["react-dom<br/>(DOM Renderer)"]
    domBindings["react-dom-bindings<br/>(DOM Host Config + Events)"]
    native["react-native-renderer<br/>(Native Renderer)"]
    scheduler["scheduler<br/>(Priority Queue)"]

    dom --> reconciler
    dom --> domBindings
    dom --> react
    native --> reconciler
    native --> react
    reconciler --> shared
    reconciler --> scheduler
    react --> shared
    domBindings --> shared

这里有一个关键点值得特别注意:react 包不依赖任何渲染器。它导出的 useState 等 Hooks 只是桩函数,实际会委托给当前安装的 dispatcher——但 react 本身并不关心这个 dispatcher 来自 react-dom 还是 react-native-renderer

ReactClient.js./ReactHooks 导入 Hooks,后者通过 ReactSharedInternals.H 读取当前的 dispatcher。Hooks 的真正实现在 reconciler 中。这一间接调用机制,正是 React 多渲染器架构的基石。

shared/ 中的桥接模块 ReactSharedInternals.js 做的事情很简单——从 react 导入并再导出:

import * as React from 'react';
const ReactSharedInternals =
  React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
export default ReactSharedInternals;

但这会在构建 react 包本身时产生循环依赖——react 不能导入 react。这正是 fork 系统登场的时机。

Fork 系统——编译时依赖注入

React 最具特色的架构模式是 fork 系统,定义于 scripts/rollup/forks.js。它根据构建类型和入口点,在编译阶段完成模块路径的替换。

forks 对象将源文件路径映射到返回替换路径的函数:

flowchart LR
    A["Import: shared/ReactSharedInternals"] --> B{Which bundle?}
    B -->|"entry = 'react'"| C["react/src/ReactSharedInternalsClient.js"]
    B -->|"entry = 'react/src/ReactServer.js'"| D["react/src/ReactSharedInternalsServer.js"]
    B -->|"condition = 'react-server'"| E["react-server/src/ReactSharedInternalsServer.js"]
    B -->|"Other packages"| F["shared/ReactSharedInternals.js (no replacement)"]

主要有三处 fork:

1. ReactSharedInternals——解决循环依赖。构建 react 包时,对 shared/ReactSharedInternals 的导入会被直接替换为 ReactSharedInternalsClient.js 的源码,其中定义了共享状态对象,并使用了简短的属性名(H 对应 hooks dispatcher,A 对应 async dispatcher,T 对应 transition,S 对应 startTransition callback,G 对应 gesture)。

2. ReactFeatureFlags——按环境区分的 flag 值。主 flag 文件 packages/shared/ReactFeatureFlags.js 定义了默认值,但每种构建类型都有各自对应的 fork。forks.js 中关于 feature flags 的处理逻辑会根据入口点和构建类型进行分发。

3. ReactFiberConfig——host config 注入。源文件 ReactFiberConfig.js 是一个哨兵式的抛错文件:

throw new Error('This module must be shimmed by a specific renderer.');

这个文件必须在构建时被替换。fork 系统会查找渲染器在 inlinedHostConfigs 中的配置条目,找到对应的 fork 文件——对于 DOM 渲染器,就是 ReactFiberConfig.dom.js,它将实现从 react-dom-bindings 中再导出。

提示: 阅读 reconciler 代码时,如果看到从 ./ReactFiberConfig 导入的内容,请记住这些是抽象操作——运行时并不会实际命中那个抛错的哨兵文件。构建系统已经将它们替换为渲染器专属的实现。这就是 React 的依赖注入。

Feature Flags 作为架构手段

React 同时向四种不同的环境发布:开源 npm、Meta 内部 Web(FB_WWW)、Meta 内部 React Native(RN_FB),以及 React Native 开源版(RN_OSS)。Feature flags 让新特性能够在这四个环境中逐步推进。

主 feature flags 文件按生命周期阶段对 flag 进行分组,并附有清晰的注释:

生命周期阶段 含义 示例
Land or remove(零成本清理) 已准备好清理 (当前为空)
Killswitch(熔断开关) 刚上线,若出现回归可随时关闭 disableSchedulerTimeoutInWorkLoop
Ongoing experiments(进行中的实验) 正在积极开发中 enableYieldingBeforePassiveenableGestureTransition
Ready for next major(下一个主版本) 将在下一个破坏性版本中发布 各种 __NEXT_MAJOR__ flag

Meta Web 属性对应的 ReactFeatureFlags.www.js fork 展示了一种混合策略:部分 flag 在运行时通过 require('ReactFeatureFlags') 动态加载,同时对特定值保留静态覆盖。这样,Meta 的基础设施就可以按员工或按比例进行灰度发布。

在构建阶段,feature flags 会被内联为 truefalse 常量,打包器的 dead code elimination 随即移除不可达分支,因此被禁用的特性在生产包中不占任何字节。

构建流水线与产物类型

构建流水线由 scripts/rollup/bundles.js 统一编排,其中定义了两组关键枚举:

模块类型(Module Types):描述正在构建的包的种类:

flowchart TD
    ISO["ISOMORPHIC<br/>react, react-jsx-runtime<br/>Works everywhere"]
    REND["RENDERER<br/>react-dom, react-native-renderer<br/>Bundles the reconciler"]
    RECON["RECONCILER<br/>react-reconciler (standalone npm)<br/>For custom renderers"]
    RUTIL["RENDERER_UTILS<br/>Helpers accessing renderer internals"]

产物类型(Bundle Types):描述目标环境和构建模式:

产物类型 目标 示例
NODE_DEV / NODE_PROD 开源 npm,开发/生产模式 react-dom.development.js
FB_WWW_DEV / FB_WWW_PROD Meta 内部 Web 使用动态 flags 的定制构建
RN_FB_DEV / RN_FB_PROD Meta 内部 React Native 内部移动端构建
RN_OSS_DEV / RN_OSS_PROD React Native 开源版 发布至 npm
ESM_DEV / ESM_PROD ES module 构建 面向现代打包器

inlinedHostConfigs.js 将渲染器短名称映射到其入口点及允许导入的路径列表。例如 dom-browser 配置项中列出了 react-domreact-dom/clientreact-server-dom-webpack/src/server/react-flight-dom-server.browser 等入口,以及所有允许导入的路径。

flowchart LR
    Source["Source Files<br/>(Flow-typed JS)"] --> Rollup["Rollup Build"]
    Rollup --> Forks["Fork System<br/>(Module Replacement)"]
    Forks --> Flags["Feature Flag Inlining<br/>(Dead Code Elimination)"]
    Flags --> Bundles["Output Bundles"]
    Bundles --> NPM["npm packages<br/>(NODE_DEV/PROD)"]
    Bundles --> FB["Meta internal<br/>(FB_WWW)"]
    Bundles --> RN["React Native<br/>(RN_FB/RN_OSS)"]

从源码到发布包的完整路径如下:Rollup 读取入口点 → fork 系统根据 (bundleType, entry) 组合替换模块 → Babel 去除 Flow 类型注解 → feature flags 内联 → Closure Compiler(部分产物)或 Terser 进行压缩 → 输出至 build/ 目录。

提示: 阅读源码时,如果某处导入的模块路径让你感到困惑,记得先去查看 scripts/rollup/forks.js。你的渲染器实际使用的模块,很可能与磁盘上该导入路径所指向的文件完全不同。

下一步

有了这张全局地图,你已经清楚 React 各个包的位置、它们之间的关系,以及构建流水线如何将它们转化为面向不同目标的产物。下一篇文章,我们将聚焦整个代码库中最核心的数据结构:Fiber 节点——这个可变的 JavaScript 对象,承载着 React 树中每一个组件、DOM 元素和边界的状态。