本篇已收录到掘金专栏《React 基础与进阶》,该系列目前一共 16 篇。
欢迎围观我的“朋友圈”、加入“低调务实优秀中国好青年”前端社群,分享技术,带你成长。
前言
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,如果构建的时候报了这样的错误:
就说明 JDK 还没有安装,点击跳转安装后,再次尝试构建。
4. link 文件
// 进入 react-app 项目目录后
yarn link react react-dom
// 启动项目
yarn start
启动后查看源代码,我们会发现 react、react-dom 已经 link 到我们构建的 react 文件上
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 个编译错误……
但不用害怕,我们慢慢解决。
有一类报错是这样的:
这类问题都可以通过 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 个报错了:
5. 错误 1:修改 React 和 ReactDOM 引入方式
在这剩余的 5 个报错中,有一个报错是:
为了避免这个错误,我们打开 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
有两个报错如下:
按照指示,我们打开 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
还有一个报错是:
为了简单起见,我们直接关掉 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:环境变量
到这里应该就没有编译错了,但是重新运行后,页面空白,打开控制台,我们会看到报错:
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
还有报错:
首先找到报错文件: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
还有报错:
打开 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);
}
可以看到浏览器打印了:
React 系列
该系列带大家从源码的角度深入理解 React 的各个 API 和执行过程。
本篇已收录到掘金专栏《React 基础与进阶》。该系列目前一共 16 篇。
此外我还写过 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答读者问等 14 个系列文章, 全系列文章目录:github.com/mqyqingfeng…
通过文字建立交流本身就是一种缘分,欢迎围观我的“朋友圈”、加入“低调务实优秀中国好青年”前端社群,分享技术,带你成长。