Appearance
React 源码总览:包结构、运行链路与阅读顺序
很多人说想看 React 源码,但一打开仓库就容易先迷路。
原因很简单:React 不是一个单文件框架,而是一组包共同完成这件事:
- 组件和 Element 怎么表示
- 更新任务怎么组织
- 浏览器 DOM 怎么接入
- 服务端渲染怎么输出
- 调度器怎么安排优先级
所以看源码时,最重要的第一步不是记函数名,而是先建立 包结构 -> 职责 -> 运行链路 这张地图。
这篇文章重点讲:
- React 源码里最值得先认识的几个包
- 一次更新通常会穿过哪些核心文件
- 源码阅读时建议按什么顺序进入
- 现代 React 的源码主线为什么会落在
react、react-reconciler、react-dom、scheduler
1. 先建立一张源码地图
如果按职责而不是按仓库目录看,React 源码最值得先抓的是这几层:
| 包 / 目录 | 更偏负责什么 |
|---|---|
packages/react | React 对外 API、本体、Element、Hooks 入口 |
packages/react-reconciler | Fiber、调和、render/commit 主线 |
packages/react-dom | 浏览器宿主接入、客户端挂载、水合、服务端输出 |
packages/scheduler | 调度与优先级相关基础能力 |
packages/shared | 各包共享的常量、工具和类型辅助 |
🌟 这一层的分工大致是:react 负责定义“React 是什么”,react-reconciler 负责“React 怎么更新”,react-dom 负责“React 怎么接到浏览器或服务端”,scheduler 负责“React 怎么安排工作”。
2. 为什么不能只看 react-dom
很多人第一次想找“React 怎么渲染到页面上”,就直奔 react-dom。
这当然不算错,但如果只看这一层,很容易缺一大块上下文。
因为真正的更新主线通常是:
react先定义 JSX / Element / Hook 入口react-reconciler负责把更新工作组织成 Fiber 树scheduler负责调度优先级和执行时机react-dom负责把最终结果提交到 DOM
也就是说,react-dom 更像宿主接入层,而不是全部 React 内核。
2.1 这几个核心包各自的能力边界
源码阅读时,最容易混的不是文件名,而是“这个包到底负责到哪”。
下面这张表更适合用来建立边界感:
| 包 | 主要负责什么 | 不主要负责什么 |
|---|---|---|
react | 对外 API、Element 模型、Hook 入口、组件模型抽象 | 不直接负责 DOM 操作,也不承载完整调和主循环 |
react-reconciler | Fiber、render/commit 主线、diff、Hook 运行时、更新计算 | 不直接操作浏览器 DOM,也不是最终调度器实现 |
react-dom | 浏览器宿主接入、root 创建、水合、DOM commit 落地、服务端输出接口 | 不负责完整 Element 创建,也不独立决定全部更新优先级 |
scheduler | 任务调度、优先级回调安排、让工作可分批执行 | 不知道组件长什么样,也不直接处理 Fiber 树结构 |
shared | 共享常量、符号、工具函数、内部通用辅助 | 不承载某条业务主线或渲染主线 |
这张表最重要的作用,是避免把几层职责全部压成一句“React 在渲染页面”。
放回各自位置看,这几层分别是:
react定义的是模型入口react-reconciler计算的是更新过程react-dom落地的是宿主操作scheduler安排的是执行时机
如果这 4 层不分开,后面看 createRoot、beginWork、dispatchSetState 时就很容易串线。
3. 一次更新穿过哪些核心主线
可以把 React 的一次更新压缩成这条线:
mermaid
flowchart TD
A[JSX / 组件执行] --> B[React Element]
B --> C[调度更新]
C --> D[Fiber 树构建与复用]
D --> E[beginWork / completeWork]
E --> F[commit]
F --> G[DOM 更新]
C --> H[scheduler]
F --> I[react-dom]如果把它映射到更常见的源码文件,大致会落在这些位置:
| 主线 | 常见源码文件 |
|---|---|
| Element 创建 | packages/react/src/jsx/ReactJSXElement.js |
| Fiber 节点定义 | packages/react-reconciler/src/ReactFiber.js |
| Hook 逻辑 | packages/react-reconciler/src/ReactFiberHooks.js |
| 调和主循环 | packages/react-reconciler/src/ReactFiberWorkLoop.js |
| 向下处理子树 | packages/react-reconciler/src/ReactFiberBeginWork.js |
| 向上收集副作用 | packages/react-reconciler/src/ReactFiberCompleteWork.js |
| commit 阶段 | packages/react-reconciler/src/ReactFiberCommitWork.js |
| 客户端根节点 | packages/react-dom/src/client/ReactDOMRoot.js |
这里最重要的不是强行背文件名,而是先知道 React 不是一个函数做完全部工作,而是被拆成了 Element、Fiber、Hook、调度、commit、宿主接入这几层。
3.1 看 4 段最值得建立感觉的源码骨架
上面那张图解决的是“地图”问题,但很多人看完还是会觉得不够落地,因为还没真正碰到源码形状。
所以在进入后面几篇专题之前,更适合看 4 段精简后的核心片段。它们不是 React 仓库里的逐字符原文,而是保留主线后的阅读辅助版本,重点是让你抓住:
- React Element 到底是什么对象
- Fiber work loop 到底怎么推进
- Hook 为什么必须按顺序挂到 Fiber 上
- 更新为什么总会回到 root 调度
3.1.1 React Element:看“JSX 最后产出什么”
js
function ReactElement(type, key, ref, props) {
return {
$$typeof: REACT_ELEMENT_TYPE, // 标记这是不是 React Element
type, // 可能是 "div",也可能是函数组件
key, // 同层节点身份标识
ref, // 命令式引用入口
props, // 普通输入数据和 children
_owner: null
};
}这段骨架最值得先记住的是:JSX 最终先变成一份描述对象,而不是直接变成 DOM。
也就是说,React 前面先解决的是“要渲染什么”,后面才继续解决“怎么更新它”和“怎么落到宿主环境”。
3.1.2 Fiber work loop:看“更新是怎么一步步跑起来的”
js
function workLoop() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork) {
const next = beginWork(unitOfWork); // 向下处理当前节点和子树
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork); // 子树处理完了,开始向上收口
} else {
workInProgress = next;
}
}这段骨架的价值在于,它把 render 阶段压缩成了一条非常清楚的线:
- 先
beginWork向下展开 - 子节点走完后再向上
complete - 整个 render 阶段本质上是在反复推进
workInProgress
🌟 所以 Fiber 主线不是“神秘黑盒”,而是 沿着 Fiber 树不断向下处理、向上收口的一套工作循环。
3.1.3 Hook 挂载:看“为什么调用顺序不能乱”
js
function mountWorkInProgressHook() {
const hook = {
memoizedState: null, // 当前 Hook 记住的状态
queue: null, // 这个 Hook 对应的更新队列
next: null // 指向下一个 Hook,多个 Hook 会串成链表
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = hook; // 第一个 Hook 挂到 Fiber 上
workInProgressHook = hook;
} else {
workInProgressHook.next = hook; // 后续 Hook 接到链表后面
workInProgressHook = hook;
}
return hook;
}这段骨架最重要的不是记字段,而是看懂 Hook 是按调用顺序一个个挂到当前 Fiber 上的。
这也正是为什么:
- 不能把 Hook 放进条件分支
- 不能有时多调一个、有时少调一个
因为 React 不是按名字找 Hook,而是按调用顺序沿着这条链表往后取。
3.1.4 调度入口:看“为什么更新最后都会回到 root”
js
function scheduleUpdateOnFiber(root, fiber, lane) {
markRootUpdated(root, lane); // 把这次更新标记到 root
ensureRootIsScheduled(root); // 再决定这批更新接下来怎么被调度执行
}
function ensureRootIsScheduled(root) {
const nextLanes = getNextLanes(root); // 选出下一批最该处理的更新
const priority = lanesToSchedulerPriority(nextLanes);
scheduleCallback(priority, () => {
performConcurrentWorkOnRoot(root);
});
}这段骨架帮助你先建立一个特别重要的认识:React 不是哪个组件 setState 了,就只在那个组件局部偷偷改一改。
更常见的主线其实是:
- 当前 Fiber 上产生更新
- 更新一路归并到 root
- root 再根据 lane 和优先级决定什么时候进入下一轮 render
所以源码里很多看起来“绕回根节点”的动作,并不是多余,而是 React 统一调度的关键。
3.2 看完这 4 段后,建议带着这几个问题继续读
如果上面 4 段骨架已经有感觉了,后面再进入专题时,最值得继续追的是:
- React Element 怎么从
jsx/createElement真正构造出来 beginWork和completeWork分别在不同 Fiber 类型上做了什么useState的更新队列怎么和 Hook 链表连起来- lane 怎么把“更新类别”和“调度优先级”串到一起
这样后面再看具体源码文件,就不容易陷入“函数名很多,但不知道它们到底接在哪条线上”的状态。
4. 看源码时最容易混的 3 组概念
4.1 react 和 react-dom
react更偏 API 和模型react-dom更偏浏览器 / 服务端宿主接入
4.2 Fiber 和 scheduler
- Fiber 更偏“更新工作的数据结构和执行模型”
- scheduler 更偏“任务怎么调度、谁先做”
4.3 render 和 commit
- render 阶段更偏“计算下一棵树”
- commit 阶段更偏“真正落地变更”
这 3 组不先区分清楚,源码很容易看成一团。
4.1 一次更新里,哪些能力分别归谁
如果把一次状态更新拆成更细一点的责任分工,可以这样看:
- 组件执行、JSX 产出 Element:主要属于
react - 把更新组织进 Fiber、比较差异、收集副作用:主要属于
react-reconciler - 安排任务优先级和执行窗口:主要依赖
scheduler - 把最终变更真正落到浏览器 DOM:主要属于
react-dom
这条分工很重要,因为它能回答几个高频误区:
为什么
react包本身并不直接改 DOM
因为 DOM 是宿主环境,归react-dom处理。为什么
scheduler不能单独解释 React 更新
因为它解决的是“什么时候做”,不是“具体怎么算”。为什么
react-dom不是完整内核
因为它承接的是宿主接入,而不是整条调和主线。
源码阅读时如果看到一个函数,先问自己:
- 它是在描述 UI 模型
- 还是在计算 Fiber 更新
- 还是在调度任务
- 还是在落地宿主操作
这样比直接背函数名更稳。
5. 一份更适合入门的阅读顺序
如果你是第一次系统读 React 源码,更阅读顺序:,而不是直接冲最深的 work loop。
5.1 看 Element
先理解:
- JSX 最终变成什么
React.createElement/jsx最终返回什么对象
这样后面再看 Fiber,你才知道“树上的节点一开始从哪里来”。
5.2 再看 Fiber 数据结构
先弄清:
- Fiber 节点长什么样
- 它和 React Element 的关系
child / sibling / return为什么是这套链表式结构
5.3 再看 render / complete / commit
这时再看:
beginWorkcompleteWorkcommitRoot
就不会只剩名词。
5.4 再看 Hooks
因为 Hook 的挂载和更新,本质上也建立在当前 Fiber 和渲染过程之上。
5.5 最后看 react-dom 和 scheduler
这时再去看:
createRoothydrateRoot- 优先级和调度
层次会清楚很多。
6. 一个“先别急着钻太深”的判断标准
如果你现在还不能稳定回答这几个问题,就不建议一开始就钻进最深层函数:
- JSX 最后返回的是 DOM 还是对象
- React Element 和 Fiber 是什么关系
- render 阶段和 commit 阶段在做什么
- 为什么 Hook 必须保持调用顺序稳定
createRoot和hydrateRoot分别接在哪条链路上
这些主线没串起来,源码越看越碎。
7. 接下来建议按这几篇继续读
如果顺着当前 React 源码线往下看,比较自然的顺序是:
- ReactElement、JSX 与 createElement 源码主线
- Fiber 数据结构与 render/commit 源码主线
- Hooks 源码:useState、useEffect 与 Hook 链表
- react-dom、scheduler 与更新调度源码主线
8. 一句话总结
React 源码最值得先建立的认识是 它不是一个“渲染函数”,而是由 react、react-reconciler、react-dom、scheduler 这些包共同组成的一套组件模型、调和模型、宿主接入和调度系统。