最近在深入学习React的源码以及原理,这里将学到的知识记录下来。暂时只学到了基本过程,详细的内部函数原理后面有机会再记录下来。
一、编译JSX
首先,JSX本质其实是javascript的语法扩展,和模板语言非常接近,但是其充分具备javascript的能力。但是其要在javascript生效的话,需使用到 Babel 进行编译,JSX在被编译后,会变成一个针对 React.createElement 的调用。
二、React.createElement 内部流程
首先,React.createElement 会接收三个参数:
第一个参数为 type,字符串类型,用于标识节点的类型,比如,div、p等,也可以是React组件类型或 React fragment类型。
第二个参数为 config,这是一个对象类型的参数,组件的所有属性都会键值对的形式存储在config中。
第三个参数为 children,这也是一个对象类型的参数,它记录的是组件标签之间嵌套的内容,也就是所谓的子节点或子元素。
React.createElement 源码:
/**
101. React的创建元素方法
*/
export function createElement(type, config, children) {
// propName 变量用于储存后面需要用到的元素属性
let propName;
// props 变量用于储存元素属性的键值对集合
const props = {};
// key、ref、self、source 均为 React 元素的属性,此处不必深究
let key = null;
let ref = null;
let self = null;
let source = null;
// 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];
}
}
}
// 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;
}
// 处理 defaultProps
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
整体来说,createElement的大致流程为:
1.二次处理key、ref、self、source四个属性值;
2.遍历config,筛选可以提到props中的属性;
3.将children中的子元素推入childArray数组;
4.格式化defaultProps
5.将以上数据作为入参,发起ReactElement的调用,最终由ReactElement返回虚拟Dom对象
三、最终将虚拟Dom传入ReactDom.render函数中,将其转变为真实Dom
上面我们讲到React.createElement会将最后处理的值传给ReactElement,其实ReactElement拿到值过后,只是做了非常简单的操作——组装。
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;
};
将所有从React.createElement中收到的值组装成一个React的虚拟Dom,最终调用ReactDom.render方法去实现转化。
ReactDom.render 接收三个参数,其中第一个参数便是生成的虚拟Dom,第二个参数则是一个真实Dom,此Dom相当于是一个容器
,React元素将被渲染到这个容器里面去,第三个参数则是一个callback function。
由此,整个从JSX到真实Dom的基本过程就完成。大致流程为:
书写JSX代码 => Babel编译JSX => 编译后的JSX执行React.createElement的调用 => 传入到ReactElement方法中生成虚拟Dom => 最终返回给ReactDom.render生成真实Dom