React 之如何调试源码

9,071 阅读7分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

本篇是 React 基础与进阶系列第 12 篇,关注专栏

前言

React 源码如何调试,想必大家在阅读源码的时候一定会遇到,所以本篇我们来讲讲如何进行源码调试。

官方推荐

其实 React 官方文档就提供了调试方法:

1. 创建项目

我们主要看如何对已有的 React 项目做调试,为了模拟这点,我们使用 create-react-app 先创建一个项目。

npx create-react-app react-app

2. 下载源码

现在我们下载 React 源码,存放在哪里都行,这里我存放在了和 react-app 同级目录(简单的说,都放桌面上了)

// 下载源码
git clone git@github.com:facebook/react.git

// 进入源码目录
cd react

// 安装依赖
yarn

3. 用源码构建文件

// 构建文件
yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE

// link react
cd build/node_modules/react
yarn link

// link react-dom
cd ../react-dom
yarn link

注意构建需要安装 JDK,如果构建的时候报了这样的错误:

image.png

就说明 JDK 还没有安装,点击跳转安装后,再次尝试构建。

4. link 文件

// 进入 react-app 项目目录后
yarn link react react-dom
// 启动项目
yarn start

启动后查看源代码,我们会发现 react、react-dom 已经 link 到我们构建的 react 文件上

image.png

5. 调试

现在我们可以直接修改 react 项目构建出的 react-dom.development.js 文件,在其中添加调试代码,页面会自动刷新。

6. 问题

这样虽然也很好用,但是有一个问题,那就是我们直接调试的是编译后的 react-dom.development.js,它并不是源码目录的文件,而是构建出来的打包文件。

如果我们修改了源码文件,那么我们就还要再次执行编译,打包成新的 react-dom.development.js 文件,这就比较麻烦了。

而且我们看源码看的肯定是 React 项目源文件,而不是看 react-dom.development.js 文件,如果我想直接调试 React 项目源文件,然后可以立刻生效,怎么办呢?

那么我们可以尝试下面这种方法。

webpack alias

这种方法原理很简单,利用 webpack 的 alias 将 react、react-dom 等库指向到源码文件,但是直接使用源码,运行的时候肯定会有很多问题,到时候我们见招拆招。

1. 创建 React 项目

npx create-react-app react-app
cd react-app

2. 在 React 项目里下载 React 源码

考虑到 React 的开发者们总是不断地提交代码,如果直接拉取,可能会导致乱七八糟的问题,所以我们使用已发布的稳定版本,这篇文章在发布的时候, npm 最新的版本为 18.2.0,所以我们这里是以 18.2.0 版本的代码为例:

// 这次我们将存放目录放在项目文件里的 src 目录下
cd src
// 我们下载的是带有 v18.2.0 tag 的版本
git clone --branch v18.2.0 git@github.com:facebook/react.git

3. 开启自定义配置

create-react-app 提供了自定义配置的方式,那就是使用 npm run eject,因为这是个不可恢复的操作,使用前,注意把代码先提交了,当然你不提交,create-react-app 也会提示你进行提交。

npm run eject

执行成功后,我们再看下 package.json 文件,会发生很多变化,注意在 scripts 这里,以前是:

{
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  }
}

现在变成了:

  "scripts": {
    "start": "node scripts/start.js",
    "build": "node scripts/build.js",
    "test": "node scripts/test.js"
  },

我们查看 scripts/start.js 文件,通过查看其中的代码,可以发现 webpack 的配置文件放在了 config/webpack.config.js

4. 修改 webpack alias

我们打开 config/webpack.config.js文件,搜索 alias,修改如下:

// 修改之前
alias: {
    // Support React Native Web
    // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
    'react-native': 'react-native-web',
    // Allows for better profiling with ReactDevTools
    ...(isEnvProductionProfile && {
      'react-dom$': 'react-dom/profiling',
      'scheduler/tracing': 'scheduler/tracing-profiling',
    }),
    ...(modules.webpackAliases || {}),
},

// 修改之后
alias: {
	react: path.join(paths.appSrc, 'react/packages/react'),
  'react-dom': path.join(paths.appSrc, 'react/packages/react-dom')
}

现在我们执行 npm start启动下项目,你会发现有 142 个编译错误……

image.png

但不用害怕,我们慢慢解决。

有一类报错是这样的:

image.png

这类问题都可以通过 alias 解决,最终 alias 的配置修改如下:

// 修改之后
alias: {
  react: path.join(paths.appSrc, 'react/packages/react'),
  'react-dom': path.join(paths.appSrc, 'react/packages/react-dom'),
  shared: path.join(paths.appSrc, 'react/packages/shared'),
  'react-reconciler': path.join(paths.appSrc, 'react/packages/react-reconciler')
}

再次执行 npm start,你会发现只有 5 个报错了:

image.png

5. 错误 1:修改 React 和 ReactDOM 引入方式

在这剩余的 5 个报错中,有一个报错是:

image.png

为了避免这个错误,我们打开 react-app/src/index.js,修改 React 和 ReactDOM 的引入方式:

// react-app/src/index.js

// 修改前
import React from 'react';
import ReactDOM from 'react-dom/client';

// 修改后
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';

6. 错误2:修改 Scheduler

有两个报错如下:

image.png

按照指示,我们打开 react-app/src/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 unstable_yieldValue = Scheduler.unstable_yieldValue;
export const unstable_setDisableYieldValue =
  Scheduler.unstable_setDisableYieldValue;

经过搜索,这两个常量的定义是在 react-app/src/react/packages/scheduler/src/forks/SchedulerMock.js下,我们引入并修改一下:

// react-app/src/react/packages/react-reconciler/src/Scheduler.js

// 在开头引入 SchedulerMock
import * as SchedulerMock from 'scheduler/src/forks/SchedulerMock';

// 修改前
export const unstable_yieldValue = Scheduler.unstable_yieldValue;
export const unstable_setDisableYieldValue = Scheduler.unstable_setDisableYieldValue;

// 修改后
export const unstable_yieldValue = SchedulerMock.unstable_yieldValue;
export const unstable_setDisableYieldValue = SchedulerMock.unstable_setDisableYieldValue;

7. 错误 3:关掉 ESlint

还有一个报错是:

image.png

为了简单起见,我们直接关掉 ESlint,打开 react-app/config/webpack.config.js 文件,搜索 disableESLintPlugin

// react-app/config/webpack.config.js

// 修改前
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true';
// 修改后
const disableESLintPlugin = true;

8. 错误 4:环境变量

到这里应该就没有编译错了,但是重新运行后,页面空白,打开控制台,我们会看到报错:

image.png

React 的源码里有直接使用 __DEV__等环境变量,我们直接替换掉,修改 config/env.js

// react-app/config/env.js

// 修改前
  const stringified = {
    'process.env': Object.keys(raw).reduce((env, key) => {
      env[key] = JSON.stringify(raw[key]);
      return env;
    }, {})
  };

// 修改后(只修改 __DEV__ 还会有其他的相同报错,所以我们直接一次改齐)
  const stringified = {
    'process.env': Object.keys(raw).reduce((env, key) => {
      env[key] = JSON.stringify(raw[key]);
      return env;
    }, {}),
    __DEV__: true,
    __EXPERIMENTAL__: true,
    __PROFILE__: true
  };

9. 错误 5:__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED

还有报错:

image.png

首先找到报错文件:react-app/src/react/packages/shared/ReactSharedInternals.js

import * as React from 'react';

const ReactSharedInternals =
  React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;

export default ReactSharedInternals;

通过全局搜索 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,我们最终找到 ReactSharedInternals 的定义在 react-app/src/react/packages/react/src/ReactSharedInternals.js,我们修改如下:

// react-app/src/react/packages/shared/ReactSharedInternals.js

// 修改前
const ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;

// 修改后
import ReactSharedInternals from 'react/src/ReactSharedInternals'

10. 错误 6:This module must be shimmed by a specific renderer

还有报错:

image.png

打开 src/react/packages/react-reconciler/src/ReactFiberHostConfig.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.');

可以看到这个文件是会在编译的时候被替换成对应的 host config,我们直接修改如下:

// src/react/packages/react-reconciler/src/ReactFiberHostConfig.js

// 修改前
throw new Error('This module must be shimmed by a specific renderer.');

// 修改后
export * from "./forks/ReactFiberHostConfig.dom";

11. 调试

到这里,代码应该已经可以正常运行起来了。

我们可以直接修改 react 的源码文件进行调试,浏览器会自动刷新,比如我修改了react/packages/scheduler/src/SchedulerMinHeap.js等文件:

export function push(heap: Heap, node: Node): void {
  const index = heap.length;
  heap.push(node);
  console.log(JSON.stringify(heap))
  siftUp(heap, node, index);
}

可以看到浏览器打印了:

image.png

React 系列

  1. React 之 createElement 源码解读
  2. React 之元素与组件的区别
  3. React 之 Refs 的使用和 forwardRef 的源码解读
  4. React 之 Context 的变迁与背后实现
  5. React 之 Race Condition
  6. React 之 Suspense
  7. React 之从视觉暂留到 FPS、刷新率再到显卡、垂直同步再到16ms的故事
  8. React 之 requestAnimationFrame 执行机制探索
  9. React 之 requestIdleCallback 来了解一下
  10. React 之从 requestIdleCallback 到时间切片
  11. React 之最小堆(min heap)

React 系列的预热系列,带大家从源码的角度深入理解 React 的各个 API 和执行过程,全目录不知道多少篇,预计写个 50 篇吧。