React核心源码解读 - 学习笔记
文章出处: 拉 勾 教 育 大前端 高薪训练营
1. 配置 React 源码本地调试环境
-
使用 create-react-app 脚手架创建项目
npx create-react-app react-test
-
弹射 create-react-app 脚手架内部配置
npm run eject
-
克隆 react 官方源码 (在项目的根目录下进行克隆)
git clone --branch v16.13.1 --depth=1 https://github.com/facebook/react.git src/react
-
链接本地源码
// 文件位置: react-test/config/webpack.config.js resolve: { alias: { "react-native": "react-native-web", "react": path.resolve(__dirname, "../src/react/packages/react"), "react-dom": path.resolve(__dirname, "../src/react/packages/react-dom"), "shared": path.resolve(__dirname, "../src/react/packages/shared"), "react-reconciler": path.resolve(__dirname, "../src/react/packages/react-reconciler"), "legacy-events": path.resolve(__dirname, "../src/react/packages/legacy-events") } }
-
修改环境变量
// 文件位置: react-test/config/env.js const stringified = { "process.env": Object.keys(raw).reduce((env, key) => { env[key] = JSON.stringify(raw[key]) return env }, { }), __DEV__: true, SharedArrayBuffer: true, spyOnDev: true, spyOnDevAndProd: true, spyOnProd: true, __PROFILE__: true, __UMD__: true, __EXPERIMENTAL__: true, __VARIANT__: true, gate: true, trustedTypes: true }
-
告诉 babel 在转换代码时忽略类型检查
npm install @babel/plugin-transform-flow-strip-types -D
// 文件位置: react-test/config/webpack.config.js [babel-loader] plugins: [ require.resolve("@babel/plugin-transform-flow-strip-types"), ]
-
导出 HostConfig
// 文件位置: /react/packages/react-reconciler/src/ReactFiberHostConfig.js + export * from './forks/ReactFiberHostConfig.dom'; - invariant(false, 'This module must be shimmed by a specific renderer.');
-
修改 ReactSharedInternals.js 文件
// 文件位置: /react/packages/shared/ReactSharedInternals.js - import * as React from 'react'; - const ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; + import ReactSharedInternals from '../react/src/ReactSharedInternals';
-
关闭 eslint 扩展
// 文件位置: react/.eslingrc.js [module.exports] // 删除 extends extends: [ 'fbjs', 'prettier' ]
-
禁止 invariant 报错
// 文件位置: /react/packages/shared/invariant.js export default function invariant(condition, format, a, b, c, d, e, f) { if (condition) return; throw new Error( 'Internal React error: invariant() is meant to be replaced at compile ' + 'time. There is no runtime version.', ); }
-
eslint 配置
在 react 源码文件夹中新建 .eslintrc.json 并添加如下配置
{ "extends": "react-app", "globals": { "SharedArrayBuffer": true, "spyOnDev": true, "spyOnDevAndProd": true, "spyOnProd": true, "__PROFILE__": true, "__UMD__": true, "__EXPERIMENTAL__": true, "__VARIANT__": true, "gate": true, "trustedTypes": true } }
-
修改 react react-dom 引入方式
import * as React from "react" import * as ReactDOM from "react-dom"
-
解决 vsCode 中 flow 报错
"javascript.validate.enable": false
-
可选项配置
如果你的 vscode 编辑器安装了 prettier 插件并且在保存 react 源码文件时右下角出现如下错误,按照如下步骤解决
-
全局安装 prettier
npm i prettier -g
-
配置 prettier path
Settings > Extensions > Prettier > Prettier path
-
-
__DEV__ 报错
删除 node_modules 文件夹,执行 npm install
2. 创建 React 元素
JSX 被 Babel 编译为 React.createElement 方法的调用,createElement 方法在调用后返回的就是 ReactElement,就是 virtualDOM。
2.1 createElement
文件位置:packages/react/src/ReactElement.js
/**
* 创建 React Element
* type 元素类型
* config 配置属性
* children 子元素
* 1. 分离 props 属性和特殊属性
* 2. 将子元素挂载到 props.children 中
* 3. 为 props 属性赋默认值 (defaultProps)
* 4. 创建并返回 ReactElement
*/
export function createElement(type, config, children) {
/**
* propName -> 属性名称
* 用于后面的 for 循环
*/
let propName;
/**
* 存储 React Element 中的普通元素属性 即不包含 key ref self source
*/
const props = {
};
/**
* 待提取属性
* React 内部为了实现某些功能而存在的属性
*/
let key = null;
let ref = null;
let self = null;
let source = null;
// 如果 config 不为 null
if (config != null) {
// 如果 config 对象中有合法的 ref 属性
if (hasValidRef(config)) {
// 将 config.ref 属性提取到 ref 变量中
ref = config.ref;
// 在开发环境中
if (__DEV__) {
// 如果 ref 属性的值被设置成了字符串形式就报一个提示
// 说明此用法在将来的版本中会被删除
warnIfStringRefCannotBeAutoConverted(config);
}
}
// 如果在 config 对象中拥有合法的 key 属性
if (hasValidKey(config)) {
// 将 config.key 属性中的值提取到 key 变量中
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 遍历 config 对象
for (propName in config) {
// 如果当前遍历到的属性是对象自身属性
// 并且在 RESERVED_PROPS 对象中不存在该属性
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
// 将满足条件的属性添加到 props 对象中 (普通属性)
props[propName] = config[propName];
}
}
}
/**
* 将第三个及之后的参数挂载到 props.children 属性中
* 如果子元素是多个 props.children 是数组
* 如果子元素是一个 props.children 是对象
*/
// 由于从第三个参数开始及以后都表示子元素
// 所以减去前两个参数的结果就是子元素的数量
const childrenLength = arguments.length - 2;
// 如果子元素的数量是 1
if (childrenLength === 1) {
// 直接将子元素挂载到到 props.children 属性上
// 此时 children 是对象类型
props.children = children;
// 如果子元素的数量大于 1
} else if (childrenLength > 1) {
// 创建数组, 数组中元素的数量等于子元素的数量
const childArray = Array(childrenLength);
// 开启循环 循环次匹配子元素的数量
for (let i = 0; i < childrenLength; i++) {
// 将子元素添加到 childArray 数组中
// i + 2 的原因是实参集合的前两个参数不是子元素
childArray[i] = arguments[i + 2];
}
// 如果是开发环境
if (__DEV__) {
// 如果 Object 对象中存在 freeze 方法
if (Object.freeze) {
// 调用 freeze 方法 冻结 childArray 数组
// 防止 React 核心对象被修改 冻结对象提高性能
Object.freeze(childArray);
}
}
// 将子元素数组挂载到 props.children 属性中
props.children = childArray;
}
/**
* 如果当前处理是组件
* 看组件身上是否有 defaultProps 属性
* 这个属性中存储的是 props 对象中属性的默认值
* 遍历 defaultProps 对象 查看对应的 props 属性的值是否为 undefined
* 如果为undefined 就将默认值赋值给对应的 props 属性值
*/
// 将 type 属性值视为函数 查看其中是否具有 defaultProps 属性
if (type && type.defaultProps) {
// 将 type 函数下的 defaultProps 属性赋值给 defaultProps 变量
const defaultProps = type.defaultProps;
// 遍历 defaultProps 对象中的属性 将属性名称赋值给 propName 变量
for (propName in defaultProps) {
// 如果 props 对象中的该属性的值为 undefined
if (props[propName] === undefined) {
// 将 defaultProps 对象中的对应属性的值赋值给 props 对象中的对应属性的值
props[propName] = defaultProps[propName];
}
}
}
/**
* 在开发环境中 如果元素的 key 属性 或者 ref 属性存在
* 监测开发者是否在组件内部通过 props 对象获取了 key 属性或者 ref 属性
* 如果获取了 就报错
*/
// 如果处于开发环境
if (__DEV__) {
// 元素具有 key 属性或者 ref 属性
if (key || ref) {
// 看一下 type 属性中存储的是否是函数 如果是函数就表示当前元素是组件
// 如果元素不是组件 就直接返回元素类型字符串
// displayName 用于在报错过程中显示是哪一个组件报错了
// 如果开发者显式定义了 displayName 属性 就显示开发者定义的
// 否者就显示组件名称 如果组件也没有名称 就显示 'Unknown'
const displayName =
typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
// 如果 key 属性存在
if (key) {
// 为 props 对象添加key 属性
// 并指定当通过 props 对象获取 key 属性时报错
defineKeyPropWarningGetter(props, displayName);
}
// 如果 ref 属性存在
if (ref) {
// 为 props 对象添加 ref 属性
// 并指定当通过 props 对象获取 ref 属性时报错
defineRefPropWarningGetter(props, displayName);
}
}
}
// 返回 ReactElement
return ReactElement(
type,
key,
ref,
self,
source,
// 在 Virtual DOM 中用于识别自定义组件
ReactCurrentOwner.current,
props,
);
}
2.2 ReactElement
文件位置:packages/react/src/ReactElement.js
/**
* 接收参数 返回 ReactElement
*/
const ReactElement = function (type, key, ref, self, source, owner, props) {
const element = {
/**
* 组件的类型, 十六进制数值或者 Symbol 值
* React 在最终在渲染 DOM 的时候, 需要确保元素的类型是 REACT_ELEMENT_TYPE
* 需要此属性作为判断的依据
*/
$$typeof: REACT_ELEMENT_TYPE,
/**
* 元素具体的类型值 如果是元素节点 type 属性中存储的就是 div span 等等
* 如果元素是组件 type 属性中存储的就是组件的构造函数
*/
type: type,
/**
* 元素的唯一标识
* 用作内部 vdom 比对 提升 DOM 操作性能
*/
key: key,
/**
* 存储元素 DOM 对象或者组件 实例对象
*/
ref: ref,
/**
* 存储向组件内部传递的数据
*/
props: props,
/**
* 记录当前元素所属组件 (记录当前元素是哪个组件创建的)
*/
_owner: owner,
};
// 返回 ReactElement
return element;
};
2.3 hasValidRef
文件位置:packages/react/src/ReactElement.js
/**
* 查看参数对象中是否有合法的 ref 属性
* 返回布尔值
*/
function hasValidRef(config) {
return config.ref !== undefined;
}
2.4 hasValidKey
文件位置:packages/react/src/ReactElement.js
/**
* 查看参数对象中是否有合法的 key 属性
* 返回布尔值
*/
function hasValidKey(config) {
return config.key !== undefined;
}
2.5 isValidElement
文件位置:packages/react/src/ReactElement.js
/**
* 验证 object 参数是否是 ReactElement. 返回布尔值
* 验证成功的条件:
* object 是对象
* object 不为null
* object对象中的 $$typeof 属性值为 REACT_ELEMENT_TYPE
*/
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
2.6 defineKeyPropWarningGetter
文件位置:packages/react/src/ReactElement.js
/**
* 指定当通过 props 对象获取 key 属性时报错
* props 组件中的 props 对象
* displayName 组件名称标识
*/
function defineKeyPropWarningGetter(props, displayName) {
// 通过 props 对象获取 key 属性报错
const warnAboutAccessingKey = function () {
// 在开发环境中
if (__DEV__) {
// specialPropKeyWarningShown 控制错误只输出一次的变量
if (!specialPropKeyWarningShown) {
// 通过 specialPropKeyWarningShown 变量锁住判断条件
specialPropKeyWarningShown = true;
// 指定报错信息和组件名称
console.error(
'%s: `key` is not a prop. Trying to access it will result ' +
'in `undefined` being returned. If you need to access the same ' +
'value within the child component, you should pass it as a different ' +
'prop. (https://reactjs.org/link/special-props)',
displayName,
);
}
}
};
warnAboutAccessingKey.isReactWarning = true;
// 为 props 对象添加 key 属性
Object.defineProperty(props, 'key', {
// 当获取 key 属性时调用 warnAboutAccessingKey 方法进行报错
get: warnAboutAccessingKey,
configurable: true,
});
}
2.7 defineRefPropWarningGetter
文件位置:packages/react/src/ReactElement.js
/**
* 指定当通过 props 对象获取 ref 属性时报错
* props 组件中的 props 对象
* displayName 组件名称标识
*/
function defineRefPropWarningGetter(props, displayName) {
// 通过 props 对象获取 ref 属性报错
const warnAboutAccessingRef = function () {
if (__DEV__) {
// specialPropRefWarningShown 控制错误只输出一次的变量
if (!specialPropRefWarningShown) {
// 通过 specialPropRefWarningShown 变量锁住判断条件
specialPropRefWarningShown = true;
// 指定报错信息和组件名称
console.error(
'%s: `ref` is not a prop. Trying to access it will result ' +
'in `undefined` being returned. If you need to access the same ' +
'value within the child component, you should pass it as a different ' +
'prop. (https://reactjs.org/link/special-props)',
displayName,
);
}
}
};
warnAboutAccessingRef.isReactWarning = true;
// 为 props 对象添加 key 属性
Object.defineProperty(props, 'ref', {
get: warnAboutAccessingRef,
configurable: true,
});
}
3. React 架构
React 16 版本的架构可以分为三层:调度层、协调层、渲染层。
- Scheduler (调度层):调度任务的优先级,高优任务优先进入协调器
- Reconciler (协调层):构建 Fiber 数据结构,比对 Fiber 对象找出差异, 记录 Fiber 对象要进行的 DOM 操作
- Renderer (渲染层):负责将发生变化的部分渲染到页面上
3.1 Scheduler 调度层
在 React 15 的版本中,采用了循环加递归的方式进行了 virtualDOM 的比对,由于递归使用 JavaScript 自身的执行栈,一旦开始就无法停止,直到任务执行完成。如果 VirtualDOM 树的层级比较深,virtualDOM 的比对就会长期占用 JavaScript 主线程,由于 JavaScript 又是单线程的无法同时执行其他任务,所以在比对的过程中无法响应用户操作,无法即时执行元素动画,造成了页面卡顿的现象。
在 React 16 的版本中,放弃了 JavaScript 递归的方式进行 virtualDOM 的比对,而是采用循环模拟递归。而且比对的过程是利用浏览器的空闲时间完成的,不会长期占用主线程,这就解决了 virtualDOM 比对造成页面卡顿的问题。
在 window 对象中提供了 requestIdleCallback API,它可以利用浏览器的空闲时间执行任务,但是它自身也存在一些问题,比如说并不是所有的浏览器都支持它,而且它的触发频率也不是很稳定,所以 React 最终放弃了 requestIdleCallback 的使用。
在 React 中,官方实现了自己的任务调度库,这个库就叫做 Scheduler。它也可以实现在浏览器空闲时执行任务,而且还可以设置任务的优先级,高优先级任务先执行,低优先级任务后执行。
Scheduler 存储在 packages/scheduler
文件夹中。
3.2 Reconciler 协调层
在 React 15 的版本中,协调器和渲染器交替执行,即找到了差异就直接更新差异。在 React 16 的版本中,这种情况发生了变化,协调器和渲染器不再交替执行。协调器负责找出差异,在所有差异找出之后,统一交给渲染器进行 DOM 的更新。也就是说协调器的主要任务就是找出差异部分,并为差异打上标记。
3.3 Renderer 渲染层
渲染器根据协调器为 Fiber 节点打的标记,同步执行对应的DOM操作。
既然比对的过程从递归变成了可以中断的循环,那么 React 是如何解决中断更新时 DOM 渲染不完全的问题呢?
其实根本就不存在这个问题,因为在整个过程中,调度器和协调器的工作是在内存中完成的是可以被打断的,渲染器的工作被设定成不可以被打断,所以不存在DOM 渲染不完全的问题。
4. 数据结构
4.1 Fiber
type Fiber = {
/************************ DOM 实例相关 *****************************/
// 标记不同的组件类型, 值详见 WorkTag
tag: WorkTag,
// 组件类型 div、span、组件构造函数
type: any,
// 实例对象, 如类组件的实例、原生 dom 实例, 而 function 组件没有实例, 因此该属性是空
stateNode: any,
/************************ 构建 Fiber 树相关 ***************************/
// 指向自己的父级 Fiber 对象
return: Fiber | null,
// 指向自己的第一个子级 Fiber 对象
child: Fiber | null,
// 指向自己的下一个兄弟 iber 对象
sibling: Fiber | null,
// 在 Fiber 树更新的过程中,每个 Fiber 都会有一个跟其对应的 Fiber
// 我们称他为 current <==> workInProgress
// 在渲染完成之后他们会交换位置
// alternate 指向当前 Fiber 在 workInProgress 树中的对应 Fiber
alternate: Fiber | null,
/************************ 状态数据相关 ********************************/
// 即将更新的 props
pendingProps: any,
// 旧的 props
memoizedProps: any,
// 旧的 state
memoizedState: any,
/************************ 副作用相关 ******************************/
// 该 Fiber 对应的组件产生的状态更新会存放在这个队列里面
updateQueue: UpdateQueue<any> | null,
// 用来记录当前 Fiber 要执行的 DOM 操作
effectTag: SideEffectTag,
// 存储要执行的 DOM 操作
firstEffect: Fiber | null,
// 单链表用来快速查找下一个 side effect
nextEffect: Fiber | null,
// 存储 DOM 操作完后的副租用 比如调用生命周期函数或者钩子函数的调用
lastEffect: Fiber | null,
// 任务的过期时间
expirationTime: ExpirationTime,
// 当前组件及子组件处于何种渲染模式 详见 TypeOfMode
mode: TypeOfMode,
};
4.2 WorkTag
文件位置:packages/shared/ReactWorkTags.js
type WorkTag =
| 0
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
| 9
| 10
| 11
| 12
| 13
| 14
| 15
| 16
| 17
| 18
| 19
| 20
| 21
| 22;
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2;
export const HostRoot = 3;
export const HostPortal = 4;
export const HostComponent = 5<