找到开始的着力点
正式的源码阅读,我先从react同名目录开始。进入index.js,看见所有的导出方法都是来自src/React.js。而在React.js中,导出的比较令我眼熟的方法主要有以下这些:
import {
createElement as createElementProd,
} from './ReactElement';
import {createContext} from './ReactContext';
import {lazy} from './ReactLazy';
import {forwardRef} from './ReactForwardRef';
import {memo} from './ReactMemo';
import {block} from './ReactBlock';
import {
useCallback,
useContext,
useEffect,
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useMemo,
useMutableSource,
useReducer,
useRef,
useState,
useTransition,
useDeferredValue,
useOpaqueIdentifier,
} from './ReactHooks';
所以很愉快的决定,从ReactElement.js开始看起,主要先来看createElement方法。
createElement
插曲
我先在createElement的方法下,加了一个debugger,然后运行代码。页面成功完成渲染,并没有运营到我打的debugger。这里其实就涉及到react16和react17的一个差异。
react16中,即使不使用react的api,只要用到jsx都需要这样引入import React from 'react'。因为需要通过@babel/preset-react将jsx的语法,转化为React.createElement的js代码。
而在react17中,直接通过react/jsx-runtime将jsx进行了转化,而不需要使用React.createElement了。因此我们在看react17时,直接在createElement中加入debugger,发现并没有走到。而在jsx/ReactJSXElement.js中的jsxDEV方法中加入debugger,会发现,走到了这个断点。
比较一下createElement和jsxDEV、jsx方法,会发现其代码基本是一致的,所以只是处理上的实现差异,基本的逻辑是不变的。所以我还是直接看createElement的代码。
是什么
在看createElement的代码实现前,首先是看它是干什么的。
简单的使用示例:
const el = React.createElement('div', {className: 'text'}, 'text')
const root = React.createElement('div', {className: 'root'}, el)
ReactDOM.render(root, document.getElementById('root'))
createElement的入参主要有三类:
type:需要创建的React元素的类型。可以是html的标签元素类型,也可以是react组件,或是react fragment类型
config:属性的对象
children:后续的参数都是当前元素的子节点。可以是字符串类型,及textContent节点。或者是React.createElement创建的元素(如示例中的两种情况)
createElement方法,返回ReactElement方法的结果。即返回一个React元素,供后续ReactDOM.render使用
怎么做
createElement的代码主要做了以下几件事:
- 将config中传入的ref、key、__self、__source进行单独存储
- 将除第一点中的四个属性外的其他属性,以及class组件的defaultProps中的属性都存储在props中
- 将从第三个参数开始的后续参数,看做子节点存在props.children中。如果只有一个节点则直接存,如果有多个节点,则以数组的形式存
- 返回ReactElement方法的结果,获得React Element元素
function createElement(type, config, children) {
let propName;
// 保存config参数中的属性
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
// 获取config中的属性,并保存至props参数中
if (config != null) {
// ref属性作为react中的特殊属性,单独记录在ref参数中
if (hasValidRef(config)) {
ref = config.ref;
if (__DEV__) {
warnIfStringRefCannotBeAutoConverted(config);
}
}
// key属性作为react中的特殊属性,单独记录在key参数中
if (hasValidKey(config)) {
key = '' + config.key; // 做了个转字符类型的处理
}
// 开发环境用于调试使用的属性(在ReactElement方法中的官方注释中有描述)
// 需特殊处理,单独保存起来,并传给了ReactElement
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 除ref、可以、__self、__source外的其他config中的属性,添加至props中
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// 计算子节点的数量(前两个参数为type和config。减去这两个参数,剩余的都是子节点)
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
// 只有一个子节点参数时,props.children直接是当前子节点
props.children = children;
} else if (childrenLength > 1) {
// 如果有多个子节点参数时,props.children为所有子节点的数组
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
if (__DEV__) {
// 开发环境下冻结子节点数组,放置被篡改
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
// 对于class组件类型
// 如果有defaultProps,则将其定义的属性加入props中
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
if (__DEV__) {
// 开发环境,对于通过props.key、props.ref调用的行为进行warning警告
// 因为ref、key没有在props上,而是单独存起来了
if (key || ref) {
const displayName =
typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
ReactElement方法主要就是创建React元素。通过$$typeof可以识别是否是React元素,而不能通过instanceof来识别。
ReactElement将开发环境的那一段方法去掉,可以看到,它主要做的,就是加了一个React Element的标识
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// 识别是React元素的标识
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
// 记录负责创建这个元素的组件
_owner: owner,
};
if (__DEV__) {
// 开发环境,将_store、_self、_source设为不可枚举的,方便React元素的比较
element._store = {};
Object.defineProperty(element._store, 'validated', {
configurable: false,
enumerable: false,
writable: true,
value: false,
});
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
}
总结
今天先是找到我们开始看源码的第一个落脚点——createElement。然后再通过源码调试的方式,开始看起具体实现。其实官方也写了一些英文注释,我英文不好,使用翻译软件,也能大概了解到这些代码的主要用途。
对于createElement,它的主要作用是将babel转换完的jsx,解析ref、key、子节点等,形成React元素
接下来我打算看一下createElement之后的ReactDOM.render的实现