架构概览:读懂 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 渲染器入口(createRoot、hydrateRoot) |
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(进行中的实验) | 正在积极开发中 | enableYieldingBeforePassive、enableGestureTransition |
| Ready for next major(下一个主版本) | 将在下一个破坏性版本中发布 | 各种 __NEXT_MAJOR__ flag |
Meta Web 属性对应的 ReactFeatureFlags.www.js fork 展示了一种混合策略:部分 flag 在运行时通过 require('ReactFeatureFlags') 动态加载,同时对特定值保留静态覆盖。这样,Meta 的基础设施就可以按员工或按比例进行灰度发布。
在构建阶段,feature flags 会被内联为 true 或 false 常量,打包器的 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-dom、react-dom/client、react-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 元素和边界的状态。