前言:如何读这本书,如何调试 React 源码
这本书要做的,是用一张图讲清 React 的内部机制,再带着你到源码里验证这张图。
它面向"会写 React、但没看过源码"的开发者。你不需要是内核专家,只需要:会写几行 JSX、装过 Node.js、愿意打开一个编辑器。
这本书在讲什么
React 的源码读起来难,难在没有地图。十几个包、几万行代码、递归里套递归,你不知道自己站在哪。
所以全书先给你一张地图:
这就是 React 的全部故事:一个工作循环(render + commit),一棵 Fiber 树,一组优先级。
- render 阶段:算出"界面应该长什么样"(新的 Fiber 树),可中断。
- commit 阶段:把"样子"变成现实(操作 DOM、执行 effect),不可中断。
- Scheduler + Lane:决定"什么时候算、先算谁"。
全书第一~第五部分就围绕这张图展开;第六、七部分讲 19 的并发特性与 RSC 怎么在这张图之上生长。
三种读者路径
| 你的情况 | 建议路径 |
|---|---|
| 只是想搞懂"React 到底怎么工作的" | 读第 1~6 章(Fiber 与工作循环),跳过细节章节 |
| 平时被 Hooks 的坑烦透了 | 直接跳到第三部分(Hooks),随时回翻第二部分的 diff 章节 |
| 想成为"能改源码的人" | 全部按顺序读,每章末尾的「动手实验」别跳过 |
源码在哪里:固定版本
本书分析的源码来自 React 官方仓库(facebook/react),全文锁定 v19.2.x(以 v19.2.7 为基准)。所有章节里的 <Src> 链接都指向这个版本。
两个读源码的入口:
-
monorepo 源码(本书主线):克隆仓库,切到对应 tag。
git clone https://github.com/facebook/react.gitcd reactgit checkout v19.2.7关键代码都在
packages/下。下面这份包地图是本书反复引用的坐标:包 干什么 后面重点看哪章 react对外 API、Hooks 的入口 第三部分 react-reconciler核心:Fiber、工作循环、diff 第一、二、五部分 react-dom浏览器渲染器、FiberRoot 第一、四部分 react-dom-bindingsDOM 操作、合成事件 第四部分 scheduler任务调度、时间切片 第 6、24 章 react-server/react-clientRSC 的服务端/客户端运行时 第 31 章 -
development bundle(快速验证):任何 React 19 项目里都有现成的开发版源码,
node_modules/react/cjs/react.development.js,适合随手查证某个 API 的行为。缺点是没有类型和源码注释,只适合"瞄一眼"。
调试源码的三种手段
三种手段各司其职,建议先 A 后 C,卡住时再用 B。完整的调试方法论见第 36 章(断点地图、条件断点、火焰图定位热点)。
手段 A:直接读 monorepo 源码(推荐主线)
不需要构建,clone 下来直接用编辑器打开就能读。
cd ~
git clone https://github.com/facebook/react.git
cd react
git checkout v19.2.7
然后用 VS Code(或任意编辑器)打开 ~/react/ 这个目录。packages/react-reconciler/src/ 下的文件名就是地图——工作循环在 react-reconciler/src/ReactFiberWorkLoop.js,beginWork 入口在 react-reconciler/src/ReactFiberBeginWork.js,completeWork 在 react-reconciler/src/ReactFiberCompleteWork.js。本书所有章节都按这个目录组织,源码位置统一用 <Src> 标注(点开即跳转 React v19.2.7 对应文件与行)。
怎么读最有效率?两个技巧:
- 跟着
<Src>链接走:每章讲到某个函数时,正文里会有可点击的<Src>引用,点开直接定位到源码对应行。不要从头到尾通读文件——跟着书的线索跳着看。 - 用 IDE 的"跳转到定义":善用
Ctrl/Cmd + 点击跟踪函数调用链。比如在react-reconciler/src/ReactFiberBeginWork.js里看到beginWork调了updateFunctionComponent,点进去就是函数组件的渲染逻辑。VS Code 里还可以用Ctrl/Cmd + Shift + F全局搜索函数名,快速定位所有调用点。
第一次打开 monorepo 可能会觉得文件多。别慌——你只需要看 packages/ 下面这六个目录(其余几十个包是工具链和测试,可以不看):
| 目录 | 干什么 |
|---|---|
packages/react/ | 对外 API(createElement、useState 等入口) |
packages/react-reconciler/ | 核心:Fiber、工作循环、diff |
packages/react-dom/ | 浏览器渲染器入口(createRoot) |
packages/react-dom-bindings/ | DOM 操作、合成事件 |
packages/scheduler/ | 任务调度、时间切片 |
packages/shared/ | 全仓库共用的常量和工具函数 |
手段 B:在 demo 里打断点
当你书读到一半、脑子里有个"这里到底传了什么参数?走了哪个分支?"时,断点是最直接的答案。整个过程只需要做一次——搭好后整本书的实验都能用它验证。
整个过程涉及两个目录,建议开两个终端窗口:
| 终端 | 目录 | 干什么 |
|---|---|---|
| 终端 1 | ~/react/ | 构建 React 源码 |
| 终端 2 | ~/react-demo/ | 你的测试项目,使用构建产物 |
下面每一步都标明了在哪个终端执行。
步骤 1:安装依赖(终端 1)
如果你没有 yarn,先装(Node 16+ 自带 corepack,一行搞定):
# 终端 1,任意目录
corepack enable
然后安装 React monorepo 的依赖:
# 终端 1,~/react/ 下
cd ~/react
yarn install
yarn install会下载大量依赖,大约 3-5 分钟。如果卡在某个包,试试换成国内源:yarn config set registry https://registry.npmmirror.com后再跑一遍。
步骤 2:构建 dev 版 React(终端 1)
仍在 ~/react/ 下:
# 终端 1,~/react/ 下
yarn build react/index react-dom/index scheduler/index --type=NODE_DEV
这一行做了三件事:
react/index react-dom/index scheduler/index——只构建这三个包(不需要构建整个 monorepo)--type=NODE_DEV——构建 dev 版(保留原始变量名和注释,不压缩,能打断点)- 产物放在
build/node_modules/下,目录结构跟你熟悉的 npm 包一模一样
首次构建大约 2-3 分钟。你会看到终端刷出一排排 "successfully compiled" 信息。完成后用
ls build/node_modules/验证——应该能看到react/、react-dom/、scheduler/三个目录。以后如果你只是想改某个包的代码试试效果,可以只构建那一个,比如
yarn build react/index --type=NODE_DEV,十几秒就完成。
步骤 3:创建一个最小 demo(终端 2)
# 终端 2,~/ 下
cd ~
npx create-react-app react-demo
cd react-demo
npm start
浏览器打开 http://localhost:3000,看到旋转的 React logo 就说明项目跑起来了。确认后按 Ctrl+C 停掉。
用 Vite 也行:
npm create vite@latest react-demo -- --template react。Vite 默认端口是http://localhost:5173。后面的 npm link 步骤完全一样。
步骤 4:用 npm link 把构建产物"偷梁换柱"
这是最关键的一步。npm link 做的事:把终端 1 构建出来的 React 包注册到你本机的全局 npm 缓存,然后让终端 2 的 demo 项目指向它,而不是指向从 npm 下载的版本。
第一步——在终端 1,把三个构建产物注册为"可被 link 的包":
# 终端 1,~/react/ 下
cd ~/react/build/node_modules/react
npm link
cd ../react-dom
npm link
cd ../scheduler
npm link
每条 npm link 成功后都会打印一行路径,类似:
/usr/local/lib/node_modules/react -> /home/你的用户名/react/build/node_modules/react
这行输出的意思是:全局 npm 仓库里多了一个叫 react 的东西,指向你刚构建的目录。
第二步——在终端 2,让 demo 项目用这些 link 过的包:
# 终端 2,~/react-demo/ 下
cd ~/react-demo
npm link react react-dom scheduler
npm link react react-dom scheduler 这一行没有输出(正常),它做的事是把 node_modules/react、node_modules/react-dom、node_modules/scheduler 替换成指向你构建产物的符号链接。
验证是否生效:
# 终端 2,~/react-demo/ 下
ls -l node_modules/react
预期输出——箭头右边必须指向你的构建目录:
lrwxrwxrwx 1 user user 60 ... node_modules/react -> /home/你的用户名/react/build/node_modules/react
如果箭头指向别的地方(比如 ../../.pnpm/...),说明 link 没生效,重新跑一遍 npm link react。
常见问题:Invalid hook call
如果
npm start后浏览器报 "Invalid hook call",几乎都是因为 node_modules 里存在两份 react:一份是 link 过来的,另一份是某个依赖(如@testing-library/react)间接引入的。解决:在终端 2(
~/react-demo/)跑:rm -rf node_modulesnpm installnpm link react react-dom schedulernpm start原因:
npm link顺序很重要——必须先npm install把基础依赖装上,再npm link覆盖掉 react 那几个包。
步骤 5:在浏览器里打断点
npm start 启动 demo,Chrome 里打开 http://localhost:3000,按 F12 → 顶部点 Sources 面板。
左侧文件树:展开 react-reconciler/src/(它在 webpack:// 或 vite/modules/ 下面,不同打包工具位置略有不同,但目录名 react-reconciler 不会变)→ 找到 ReactFiberBeginWork.js → 打开后搜索(Ctrl+F)function beginWork → 在这一行的行号上点一下,出现蓝色箭头。
步骤 6:触发,然后读调用栈
在页面上操作(点按钮、输入文字)。断点命中后,浏览器会停在 beginWork 的第一行。这时看 Sources 面板右侧的 Call Stack——从栈底往上读,你能看到:
beginWork ← 你断在这里
performUnitOfWork
workLoopSync
renderRootSync
...
onClick (或者别的用户事件)
这就是一次更新的完整调用链——从事件触发一路跑到 Fiber 处理。第 22 章会把这根链子拆碎了给你看,现在先感受一下它的存在。
本书不要求你每次阅读都构建源码,大部分章节"只读源码 + 跑跑 demo"就够。手段 B 是当你卡在一个具体行为时用的"手术刀"。搭好这套环境后,条件断点、跳过 DEV 噪音、在特定组件上断点等进阶技巧见第 36 章。
另外,如果你用的是 Vite 项目,还有个更快的方法——不用 npm link,直接在
vite.config.ts里加 resolve.alias:// vite.config.tsimport { defineConfig } from 'vite'import react from '@vitejs/plugin-react'export default defineConfig({plugins: [react()],resolve: {alias: {react: '/home/你的用户名/react/build/node_modules/react','react-dom': '/home/你的用户名/react/build/node_modules/react-dom',scheduler: '/home/你的用户名/react/build/node_modules/scheduler',},},})改完后重启
npm run dev即可。Vite alias 方式没有"多份 react"的隐患,省心。
手段 C:React DevTools
从 Chrome 应用商店或 Firefox Add-ons 安装扩展。装好后打开任意 React 页面,浏览器右上角会点亮 React 图标(灰色 = 当前页不是 React 项目,蓝橙色 = 是 React 项目且 DevTools 已连接)。
两个核心面板,按下 F12 → 顶部找到 Components 和 Profiler 两个新标签页:
-
Components:拼图图标。选中页面上的一个 DOM 节点,右侧面板显示它对应 fiber 的
props、state(展开hooks那一行就是第 13 章的 Hooks 链表)、render来源("rendered by XXX")。读完第 3 章"Fiber 数据结构"后回来这里,对着面板看真的 fiber,一目了然。这是把书里的图谱搬到眼前最快的方式。 -
Profiler:圆饼图图标。点顶部录制按钮(灰色圆点变成红色),操作页面(点按钮、切换 tab),再点一下停止。你会看到一张火焰图——横轴是时间,竖条越高 = 渲染越久。鼠标悬停在柱子上显示渲染时长和原因,这是第 25 章(bailout)和第 27 章(并发渲染)最直观的验证工具:火焰图里越粗的柱子,越值得翻回源码查为什么没跳过。
试试这个:打开 Components 面板,在你用
create-react-app建的 demo 里随便选一个<p>标签,看右侧 panels——你已经看到 React 内部数据了。读完第 3 章后回来,你会认出memoizedProps和tag。
阅读约定
<Src>引用:形如react-reconciler/src/ReactFiberWorkLoop.js:1700的可点击链接,指向 Reactv19.2.7源码的对应文件与行。正文里会频繁出现,点开即可对照。- 代码块:带
// →注释的是"这行是关键",建议跟读;其余可跳读。 - 图例:所有图都是 Mermaid 手绘风(马卡龙配色)。语义靠形状区分:
[]矩形 = 节点/状态,{}菱形 = 判断分支,()圆角 = 入口/出口,(( ))圆 = 事件/触发;淡紫连线 = 更新主路径。 - 选讲章节:正文标注"选讲"的章节,与主线索弱相关,赶时间可跳过。
动手:跑一个最小 demo
整本书讲的工作循环,都可以回到这个最小例子来验证。点下面的按钮试试,这就是 useState 的现场:
function LiveCounter() { const [count, setCount] = useState(0); return <button onClick={() => setCount((c) => c + 1)}>点了 {count} 次</button>; }
你每点一次按钮,React 就走完一遍「render → commit」的工作循环——这正是第一部分要图解的东西。
准备好了就翻开第一部分吧。我们从一个 createRoot() 开始。