JSX如何变成DOM

入门React的时候,我们了解了神奇的JSX语法,当时的官方是建议使用JSX,作为小白的我当然是乖乖听话。想必现在大家都早已经习惯使用JSX,我们今天来了解一下它是如何成为DOM的。

JSX的本质

官方描述JSX是JavaScript的扩展语法,它充分具备JavaScript的能力,那么它是怎么做到的呢。这时候我能想到的就是官方霸霸给出的一句话,JSX会被编译成React.createElement(),它会返回一个叫做’React Element’的JS对象。首先编译这个动作,它是由Babel来完成的,我们知道Babel的主要功能是将ECMAScript2015+版本的代码转换成向后兼容的JavaScript语法,从而能运行在当前的浏览器中。其实,JSX也是由Babel来转换为Javascript代码的。

我随便找了一段项目里的简单组件的JSX代码放在Babel官网上,它会将其转换成React.createElement的调用。可以看到,所有的JSX标签都被转换成了React.createElement调用,大家明显能感受到,JSX相对而言不仅阅读起来友好,开发起来也比较简单(2333这个比较关键)。小结一下,JSX本质是React.createElement这个JavaScript调用的语法糖,它允许开发者用较熟悉的类HTML标签语法来创建虚拟DOM,提升了开发效率,也降低了学习成本。

读一读createElement源码

1
export function createElement(type, config, children)

这个方法有三个入参,type是节点类型,可以是div、span这样的标准HTML标签字符串,也可以是React组件类型;config是一个对象,以键值对的形式存储了组件的属性;children记录的是子节点、子元素对象。

1
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
48
49
50
51
52
53
54
55
56
57
58
//propName用于存储元素属性,props用于存储属性的键值对集合
let propName;
const props = { };
let key = null;
let ref = null;
let self = null;
let source = null;
// key、ref、self、source 均为 React 元素的属性
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;

//接下来将符合的config里的属性放入props中
for (propsName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}

//childrenLength是在获取子元素,因此减去的两项是指type和config两个参数占用的长度
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];
}
//将children赋值给props的children属性
props.children = childArray;
}

//别忘了处理节点的默认属性
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好像也没做啥哈哈哈,它并没有涉及到真实的DOM。其实它只是接受相对简单的参数然后做一次数据处理,最后调用ReactElement来创建元素。那么我们接下来继续康康ReactElement的源码。

读一读ReactElement源码,认识一下虚拟DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//通过React.createElement中调用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;
};

我们可以发现,ReactElement的逻辑也较为简单,它只是按一定规范组装了一个element对象,通过React.createElement最终返回到了开发者。我们平常去打印一个React元素,就会发现它是一个标准的ReactElement对象实例,如图所示。

这个 ReactElement 对象实例,本质上是以 JavaScript 对象形式存在的对 DOM 的描述,也就是大家常说的虚拟Dom。既然是虚拟的,那它离页面的真实DOM还有一定距离,最终是由ReactDom.render方法来填补的。这个方法可以传入三个参数,需要渲染的元素element,元素挂载的目标容器container也就是真实的DOM节点,以及可选参数回调函数。