前置知识

# 源码的文件结构
# 顶层目录
除去配置文件和隐藏文件夹,根目录的文件夹有三个:
根目录
├── fixtures # 包含一些给贡献者准备的小型 React 测试项目
├── packages # 包含元数据(比如 package.json)和 React 仓库中所有 package 的源码(子目录 src)
├── scripts # 各种工具链的脚本,比如git、jest、eslint等
2
3
4
这里关注packages目录
# react文件夹 (opens new window)
React的核心,包含所有全局的API,如:
React.createElementReact.ComponentReact.Children这些API全平台通用,但不包含ReactDOM、ReactNative等平台特定的代码。在npm上作为单独包 (opens new window)发布。
# scheduler文件夹 (opens new window)
Scheduler(调度器)的实现
# shared文件夹 (opens new window)
源码中其他模块公用的方法和全局变量,如shared/ReactSymbols.js (opens new window)中保存React不同组件类型的定义。
// ...
export let REACT_ELEMENT_TYPE = 0xeac7;
export let REACT_PORTAL_TYPE = 0xeaca;
export let REACT_FRAGMENT_TYPE = 0xeacb;
// ...
2
3
4
5
# Renderer相关的文件夹
- react-art
- react-dom # 注意这同时是DOM和SSR(服务端渲染)的入口
- react-native-renderer
- react-noop-renderer # 用于debug fiber(后面会介绍fiber)
- react-test-renderer
2
3
4
5
# 实验性包的文件夹
React将自己流程中的一部分抽离出来,形成可独立使用的包。 由于是实验性质,所不建议在生产中用。
- react-server # 创建自定义SSR流
- react-client # 创建自定义的流
- react-fetch # 用于数据请求
- react-interactions # 用于测试交互相关的内部特性,比如React的事件模型
- react-reconciler # Reconciler的实现,你可以用他构建自己的Renderer
2
3
4
5
# 辅助包的文件夹
React将一些辅助功能形成单独包
- react-is # 用于测试组件是否是某类型
- react-client # 创建自定义的流
- react-fetch # 用于数据请求
- react-refresh # “热重载”的React官方实现
2
3
4
# react-reconciler文件夹 (opens new window)
重点关注这个包,虽然它是一个实验包,内部很多功能在正式版还未开发,但它一边对接Scheduler,一边对接不同平台的Renderer,构成了React16的架构体系。
# 调试源码
即使版本号相同(当前最新版为17.0.0 RC),但是facebook/react项目master分支的代码和我们使用create-react-app创建的项目node_modules下的react项目代码还是有些区别。
因为React的新代码都是直接提交到master分支,而create-react-app内的react使用的是稳定版的包。
为了始终使用最新版React教学,我们调试源码遵循以下步骤:
- 从
facebook/react项目master分支拉取最新源码 - 基于最新源码构建
react、scheduler、react-dom三个包 - 通过
create-react-app创建测试项目,并使用步骤2创建的包作为项目依赖的包
# 拉取源码
# 拉取代码
git clone https://github.com/facebook/react.git
# 如果拉取速度很慢,可以考虑如下2个方案:
# 1. 使用cnpm代理
git clone https://github.com.cnpmjs.org/facebook/react
# 2. 使用码云的镜像(一天会与react同步一次)
git clone https://gitee.com/mirrors/react.git
2
3
4
5
6
7
8
9
10
安装依赖
# 切入到react源码所在文件夹
cd react
# 安装依赖
yarn
2
3
4
5
打包react、scheduler、react-dom三个包为dev环境可以使用的cjs包。
# 执行打包命令
yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE
2
在源码目录build/node_modules下会生成最新代码的包。为react、react-dom创建yarn link。
通过yarn link可改变项目中依赖包的目录指向
cd build/node_modules/react
# 申明react指向
yarn link
cd build/node_modules/react-dom
# 申明react-dom指向
yarn link
2
3
4
5
6
# 创建项目
通过create-react-app创建项目,并将react,react-dom包指向刚才生成的包。
npx create-react-app a-react-demo
# 将项目内的react react-dom指向之前申明的包
yarn link react react-dom
2
3
现尝试在react/build/node_modules/react-dom/cjs/react-dom.development.js中随意打印些东西。
启动项目,可在浏览器控制台看到打印输出的东西了。
# 深入理解JSX
JSX作为描述组件内容的数据结构,为JS赋予了更多视觉表现力。 那么,有些疑问:
- JSX和Fiber是同一个东西吗
- React Component、React Element是同一个东西吗,和JSX有什么关系?
# JSX简介
官网对其的描述 (opens new window)。
JSX在编译时会被Babel编译为React.createElement方法。
这也是为什么在每个使用JSX的文件中,必须显式声明,否则在运行时该模块会报react未定义的错,不过React17已解决了此问题,无需显式声明。
详见JSX转换 (opens new window)
import React from 'react';
JSX 并不只能被编译为React.createElement方法,可通过@babel/plugin-transform-react-jsx (opens new window)插件告诉Babel编译时需将JSX编译为什么函数的调用(默认为React.createElement)
如在parent这个类react的库中 ,JSX会被编译为h的函数调用
// 编译前
<p>KaSong</p>
// 编译后
h("p", null, "KaSong");
2
3
4
# React.createElement
JSX 被编译为React.createElement都做了什么:
export function createElement(type, config, children) {
let propName;
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
// 将 config 处理后赋值给 props
// ...省略
}
const childrenLength = arguments.length - 2;
// 处理 children,会被赋值给props.children
// ...省略
// 处理 defaultProps
// ...省略
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// 标记这是个 React Element
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
return element;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
可看到,React.createElement最终会调用ReactElement方法返回一个包含组件数据的对象。
该对象有个参数$$typeof: REACT_ELEMENT_TYPE标记了该对象是个React Element。
所以调用React.createElement返回的对象就是React Element么?
React提供了验证合法React Element的全局API React.isValidElement (opens new window),看下它的实现:
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
2
3
4
5
6
7
所以,在React中,所有JSX在运行时的返回结果(即React.createElement()的返回值)都是React Element。
# React Component
在React中,常用ClassComponent与FunctionComponent构建组件。
class AppClass extends React.Component {
render() {
return <p>KaSong</p>
}
}
console.log('这是ClassComponent:', AppClass);
console.log('这是Element:', <AppClass/>);
function AppFunc() {
return <p>KaSong</p>;
}
console.log('这是FunctionComponent:', AppFunc);
console.log('这是Element:', <AppFunc/>);
2
3
4
5
6
7
8
9
10
11
12
13
14
从控制台可看到,ClassComponent对应的Element的type字段为AppClass自身。
FunctionComponent对应的Element的type字段为AppFunc自身:
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ƒ AppFunc(),
_owner: null,
_store: {validated: false},
_self: null,
_source: null
}
2
3
4
5
6
7
8
9
10
11
值得注意的一点,由于
AppClass instanceof Function === true;
AppFunc instanceof Function === true;
2
所以无法通过引用类型区分ClassComponent和FunctionComponent。React通过ClassComponent实例原型上的isReactComponent变量判断是否是ClassComponent。
ClassComponent.prototype.isReactComponent = {};
# JSX与Fiber节点
JSX是一种描述当前组件内容的数据结构,他不包含组件schedule、reconcile、render所需的相关信息。
如不包含如下信息(这些内容都包含在Fiber节点中):
- 组件在更新中的优先级
- 组件的
state - 组件被打上的用于
Renderer的标记
所以,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。
在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。