前言
这是React源码系列专栏的第四篇文章,预计写10篇左右,之前的文章请查看文末,通过本专栏的学习,相信大家可以快速掌握React源码的相关概念以及核心思想,向成为大佬的道路上更近一步; 本章我们学习 createElement 解析,本系列源码基于v18.2.0版本;
JSX
在React17之前,我们写React代码的时候都会去引入React,并且自己的代码中没有用到,这是为什么呢?
这是因为我们的 JSX 代码会被 Babel 编译为 React.createElement,我们来看一下babel的表示形式。
需要注意的是:
- 自定义组件时需要首字母用大写,会被识别出是一个组件,这是一个规定。
- 小写默认会认为是一个html标签,编译成字符串。
结论:JSX 的本质是React.createElement这个 JavaScript 调用的语法糖。是JS的语法扩展;
React17之后的版本 React 已经不需要引入 createElement ,这种模式来源于 Automatic Runtime,看一下是如何编译的。
function App(){
return <div>
<h1>hello,world</h1>
</div>
}
被编译后的文件:
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
function App() {
return _jsxs("div", {
children: [
_jsx("h1", {
children: "hello,world"
}),
],
});
}
plugin-syntax-jsx 已经向文件中提前注入了 _jsxRuntime api。不过这种模式下需要我们在 .babelrc 设置 runtime: automatic 。
"presets": [
["@babel/preset-react",{
"runtime": "automatic"
}]
],
createElement源码阅读
从上面我们知道jsx通过babel编译成React.createElement,下面我们就去看一下相关源码。 1、首先我们先找到 createElement 暴露位置,位于 packages/react/index 目录下;
// packages/react/index
export { createElement } from './src/React';
2、进入 packages/react/src/React 找到 createElement引入位置;
// packages/react/src/React.js
import {
createElement as createElementProd,
} from './ReactElement';
3、进入packages/react/src/ReactElement.js 找到 createElement 函数;
// packages/react/src/ReactElement.js
export function createElement(type, config, children) {
// ... 省略 ...
}
入参解读
createElement源码位于 react/packages/react/src/ReactElement.js 入参解读:创造一个元素需要知道哪些信息
export function createElement(type, config, children) { /* ... 省略 ... */}
createElement 有 3 个入参,这 3 个入参包括了 React 创建一个元素所需要知道的全部信息。
- type:用于标识节点的类型。它可以是类似“h1”“div”这样的标准 HTML 标签字符串,也可以是 React 组件类型或 React fragment 类型。
- config:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中。
- children:以对象形式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的“子节点”“子元素”。
我们将createElement 操作拆分为以下四步:
1、config参数处理
// config 对象中存储的是元素的属性
if (config != null) {
// 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值
if (hasValidRef(config)) {
ref = config.ref;
}
// 此处将 key 值字符串化
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面
for (propName in config) {
if (
// 筛选出可以提进 props 对象里的属性
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
这段代码对 ref 以及 key 做了个验证处理,具体如何验证我们先不关心,从方法名称上来辨别一下,然后遍历 config 并把属性提进 props 对象里,也就是把 ref 和 key 剔除;
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
};
2、对 children 的操作
首先把第二个参数之后的参数取出来,然后判断长度是否大于一。大于一的话就代表有多个 children,这时候 props.children 会是一个数组,否则的话只是一个对象。
// childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度
const childrenLength = arguments.length - 2;
// 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了
if (childrenLength === 1) {
// 直接把这个参数的值赋给props.children
props.children = children;
// 处理嵌套多个子元素的情况
} else if (childrenLength > 1) {
// 声明一个子元素数组
const childArray = Array(childrenLength);
// 把子元素推进数组里
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// 最后把这个数组赋值给props.children
props.children = childArray;
}
3、处理传入的defaultProps
defaultProps主要处理默认值相关操作
// 处理 defaultProps
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
4、ReactElement
最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数,
// 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
5、完整代码
export function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
if (__DEV__) {
warnIfStringRefCannotBeAutoConverted(config);
}
}
if (hasValidKey(config)) {
if (__DEV__) {
checkKeyStringCoercion(config.key);
}
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// Remaining properties are added to a new props object
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
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;
}
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
if (__DEV__) {
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,
);
}
createElement小结
createElement 中并没有十分复杂的涉及算法或真实 DOM 的逻辑,它的每一个步骤几乎都是在格式化数据。
出参解读
上面已经分析过,createElement 执行到最后会 return 一个针对 ReactElement 的调用;
const ReactElement = function(type, key, ref, self, source, owner, props) { /*省略*/}
ReactElement源码拆解
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// 内置属性赋值
type: type,
key: key,
ref: ref,
props: props,
// 记录创造该元素的组件
_owner: owner,
};
//
if (__DEV__) {
// 这里是一些针对 __DEV__ 环境下的处理,对于大家理解主要逻辑意义不大,此处我直接省略掉,以免混淆视听
}
return element;
};
$$typeof
来帮助我们识别这是一个 ReactElement
;
ReactElement小结
ReactElement 其实只做了一件事情就是组装数据。 可以在React中尝试打印:
const AppJSX = (<div className="App">
<h1 className="title">I am the title</h1>
<p className="content">I am the content</p>
</div>)
console.log(AppJSX)
得到的控制台结果:
这个 ReactElement 对象实例,本质上是以 JavaScript 对象形式存在的对 DOM 的描述,也就是虚拟 DOM;
小结
本章我们学习了 createElement 的源码,接下来的文章将进入 React 主流程分析,欢迎继续跟随本专栏一起学习;