如何调试 React 源码?

最简单的方式

克隆本仓库:

git clone https://github.com/wjgogogo/dive-react-19.git

安装依赖:

pnpm install

执行命令,启动 React 源码调试:

pnpm react:dev

还可以继续在 packages/react-dojo/dojo/index.jsx 文件中编写调试代码。

如果使用 VSCode 编辑器,在启动项目后,可直接在运行和调试面板中的选择启动 react debug,就可以使用 VSCode 断点调试 React 源码。

TIP

本仓库使用的是 React 19 开发版本,对应的 commit 为 feat: expose installHook with settings argument from react-devtools-core/backend

使用 Webpack 调试 React 源码

如果你想调试 React 最新版的代码,实时跟踪 React 进展。也可以采用和本仓库类似的方式,关键步骤如下。

新建项目,假设项目名为 react-debug

克隆 React 最新版代码

react-debug 项目根目录下,执行命令获取 React 最新版代码:

git clone https://github.com/facebook/react.git --depth=1

webpack 配置

假设项目结构为:

react-debug ├── src └── index.jsx # 调试代码 ├── react # react 源码项目 ├── webpack.config.js # webpack 配置 ├── public └── index.html ├── package.json

package.json 配置如下:

{ "scripts": { "dev": "cross-env NODE_ENV=development webpack serve" }, "devDependencies": { "@babel/preset-env": "^7.26.0", "@babel/preset-flow": "^7.24.7", "@babel/preset-react": "^7.24.7", "babel-loader": "^9.1.3", "cross-env": "^7.0.3", "css-loader": "^6.10.0", "html-webpack-plugin": "^5.6.0", "source-map-loader": "^5.0.0", "style-loader": "^3.3.4", "webpack": "^5.90.3", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.2" } }

webpack.config.js 配置如下:

const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const webpack = require("webpack"); module.exports = { mode: "development", devtool: "cheap-module-source-map", entry: "./src/index.jsx", resolve: { // 将 react 解析定位到源码文件 alias: { react: path.resolve(__dirname, "./react/packages/react"), "react-client": path.resolve(__dirname, "./react/packages/react-client"), "react-dom": path.resolve(__dirname, "./react/packages/react-dom"), "react-dom-bindings": path.resolve( __dirname, "./react/packages/react-dom-bindings" ), "react-reconciler": path.resolve( __dirname, "./react/packages/react-reconciler" ), scheduler: path.resolve(__dirname, "./react/packages/scheduler"), shared: path.resolve(__dirname, "./react/packages/shared"), }, }, module: { rules: [ { // 映射 babel 编译的代码的 source map enforce: "pre", exclude: /@babel(?:\/|\\{1,2})runtime/, test: /\.(js|mjs|jsx|css)$/, loader: "source-map-loader", }, { test: /\.(js|mjs|jsx)$/, loader: "babel-loader", options: { presets: [ "@babel/preset-env", [ "@babel/preset-react", { development: true, runtime: "automatic", }, ], // react 源码使用 flow 语法,需要 flow 插件编译 ["@babel/preset-flow"], ], }, }, { test: /\.css$/, use: ["style-loader", "css-loader"], sideEffects: true, }, ], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "./public/index.html"), }), new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development"), // react 源码中所使用的全局变量 __DEV__: true, // 用于区分开发环境和生成环境 __EXPERIMENTAL__: true, // 用于控制实验性功能的开关,实验性功能通常是下一个开发版本的具有破坏性的功能 __PROFILE__: false, // 是 React 源码中用于性能分析的全局标志,主要用于控制与性能分析相关的功能是否启用 }), ], devServer: { static: { directory: path.resolve(__dirname, "public"), }, port: 8080, hot: false, client: { progress: true, overlay: false, }, }, };

public/index.html 就是最常规的模板内容:

<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html>

源码修改

React 在编译时才会提供 renderer,可以直接跳过 React 的脚本行为,因为本文只关注 CSR,所以写死为 ReactFiberConfig.dom

react/packages/react-reconciler/src/ReactFiberConfig.js
// We expect that our Rollup, Jest, and Flow configurations // always shim this module with the corresponding host config // (either provided by a renderer, or a generic shim for npm). // // We should never resolve to this file, but it exists to make // sure that if we *do* accidentally break the configuration, // the failure isn't silent. // throw new Error('This module must be shimmed by a specific renderer.'); export * from "./forks/ReactFiberConfig.dom";

logunstable_setDisableYieldValue 函数在测试环境中才会存在,这里直接设置为空:

react/packages/react-reconciler/src/Scheduler.js
// this doesn't actually exist on the scheduler, but it *does* // on scheduler/unstable_mock, which we'll need for internal testing // export const log = Scheduler.log; // export const unstable_setDisableYieldValue = // Scheduler.unstable_setDisableYieldValue; export const log = () => {}; export const unstable_setDisableYieldValue = () => {};

React 使用 ReactSharedInternals 来共享内部功能和状态, 并且 Client 和 Server 各自一套状态,这里也直接写死为 Client 对应的版本:

react/packages/shared/ReactSharedInternals.js
// import * as React from 'react'; // const ReactSharedInternals = // React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; import ReactSharedInternals from "../react/src/ReactSharedInternalsClient"; export default ReactSharedInternals;

解决一下 flow 语法编译错误的问题:

react/packages/react-reconciler/src/ReactFiberDevToolsHook.js
// 将下面这一行的 const 关键字改成 var // declare const __REACT_DEVTOOLS_GLOBAL_HOOK__: Object | void; declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: Object | void;

最后执行命令:

pnpm dev # or npm run dev

INFO

React 最新源码会不断变化,即使完成了上述的操作,也不能保证执行一定成功,请根据报错信息再进一步更正。