如何调试 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 v19.0.0 版本,对应的 tag 为 v19.0.0

使用 Webpack 调试 React 源码

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

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

克隆 React 最新版代码

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

git clone https://github.com/facebook/react.git --branch v19.0.0 --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 最新源码会不断变化,即使完成了上述的操作,也不能保证执行一定成功,请根据报错信息再进一步更正。