文章目录
本篇文章主要讲解 JSX的来龙去脉,不讲解 JSX的语法(不了解的可参考: JSX 简介)。如果你对以下三个问题能够清晰解答,本篇文章就可以不用看了。
- JSX 的本质是什么,它和 JS 之间到底是什么关系?
- 为什么要用 JSX?不用会有什么后果?
- JSX 背后的功能模块是什么,这个功能模块都做了哪些事情?
如果不能清晰地回答,那希望看完本篇文章你能有所收获。
先举个小例子让你回忆一下JSX,以下代码中函数组件App中返回的部分即为JSX(<div className="App">Hello World!</div>
):
import React from "react";
import ReactDOM from "react-dom";
function App() {
return <div className="App">Hello World!</div>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
1. JSX的本质: React.createElement这个 JavaScript 调用的语法糖
JSX的本质是JavaScript 的语法扩展。React官方对JSX的描述:
JSX is a syntax extension to JavaScript. It is similar to a template language, but it has full power of JavaScript.
JSX 是一个 JavaScript 语法扩展。它类似于模板语言,但它具有 JavaScript 的全部能力。
这个描述的意思是使用JSX语法可以扩展JavaScript 的功能:在JavaScript 中可以像写HTML一样来构建UI(原生JS是不具备这种能力的),但编译后最终其实还是纯JS代码。JSX 的定位是 JavaScript 的“扩展”,而非 JavaScript 的“某个版本”,所以浏览器并不会像天然支持 JavaScript 一样地支持 JSX。要使JSX在JavaScript中生效,我们需要借助Babel(Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。类似的,Babel 也具备将 JSX 语法转换为 JavaScript 代码的能力。)进行编译:JSX 会被编译为 React.createElement(), React.createElement() 将返回一个叫作“React Element”的 JS 对象。
如下一个Babel编译的例子:
可以看到,所有的 JSX 标签都被转化成了 React.createElement 调用,这也就意味着,我们写的 JSX 其实写的就是 React.createElement。
这里我们可以得出结论:JSX 的本质其实是React.createElement这个 JavaScript 调用的语法糖。
2. 为什么要用 JSX: 在降低学习成本的同时,也提升了研发效率与研发体验
从上文Babel编译的例子中我们已经初步体会到使用JSX相对于直接使用JS编写代码的的简洁性差别,看起来好像还可以接受,那让我们看看稍微复杂点的例子:
可以很明显地看出,JSX 代码层次分明、嵌套关系清晰;而 React.createElement 代码则给人一种非常混乱的“杂糅感”,这样的代码不仅读起来不友好,写起来也费劲。实际项目中只会比例子更加复杂,相信没有人愿意用React.createElement的方式来编写吧?
这里我们可以得出结论:JSX 语法糖允许前端开发者使用我们最为熟悉的类 HTML 标签语法来创建虚拟 DOM,在降低学习成本的同时,也提升了研发效率与研发体验。。
3. JSX 背后的功能模块是什么,这个功能模块都做了哪些事情?
从上文我们已知所有的 JSX 标签最终都被转化成了 React.createElement 调用,那React.createElement这个函数到底做了些什么呢?让我们进入React的源码(版本17.0.1)一探究竟:
/**
* 源码位置:packages\react\src\ReactElement.js
* 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];
}
}
}
if (__DEV__) {
// 这里是一些针对 __DEV__ 环境下的处理,暂时可以不用管
}
// 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
上面源码不复杂,其实该函数并没有做很多复杂的事情,基本上是在进行格式化数据的操作,执行到最后会 return 一个针对 ReactElement 的调用。让我们通过一个流程图来更清晰地理解createElement的逻辑处理过程:
createElement 就像是开发者和 ReactElement 调用之间的一个“转换器”、一个数据处理层。它可以从开发者处接受相对简单的参数,然后将这些参数按照 ReactElement 的预期做一层格式化,最终通过调用 ReactElement 来实现元素的创建。整个过程如下图所示:
3.1 入参解读:创造一个元素需要知道哪些信息
export function createElement(type, config, children)
createElement函数包含3入参,分别如下:
- type:用于标识节点的类型。它可以是类似
h1
、div
这样的标准 HTML 标签字符串,也可以是 React 组件类型或 React fragment 类型。 - config:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中。
- children:以对象形式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的“子节点”“子元素”。
3.2 出参解读:初识虚拟 DOM
从源码中我们可以看到出参是一个针对 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 其实只做了一件事情,那就是创建,说得更精确一点,是组装:ReactElement 把传入的参数按照一定的规范,“组装”进了 element 对象里,并把它返回给了 React.createElement,最终 React.createElement 又把它交回到了开发者手中。整个过程如下图所示:
我们可以将JSX打印出来看看,来验证一下:
import React from "react";
import ReactDOM from "react-dom";
function App() {
return <div className="App">Hello World!</div>;
}
const myJSX = <div className="App">Hello World!</div>
console.log(myJSX)
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
结果如下:
这个 ReactElement 对象实例,本质上是以JavaScript 对象形式存在的对 DOM 的描述,也就是老生常谈的“虚拟 DOM”(准确地说,是虚拟 DOM 中的一个节点)。
既然是“虚拟 DOM”,那就意味着和渲染到页面上的真实 DOM 之间还有一些距离,这个“距离”,就是由大家喜闻乐见的ReactDOM.render方法来填补的。
在每一个 React 项目的入口文件中,都少不了对 ReactDOM.render 函数的调用。下面简单介绍下 ReactDOM.render 方法的入参规则:
ReactDOM.render(
// 需要渲染的元素(ReactElement)
element,
// 元素挂载的目标容器(一个真实DOM)
container,
// 回调函数,可选参数,可以用来处理渲染结束后的逻辑
[callback]
)
ReactDOM.render 方法可以接收 3 个参数,其中第二个参数就是一个真实的 DOM 节点,这个真实的 DOM 节点充当“容器”的角色,React 元素最终会被渲染到这个“容器”里面去。这个真实 DOM 一定是确实存在的。比如,在 App 组件对应的 index.html 中,已经提前预置 了 id 为 root 的根节点:
<body>
<div id="root"></div>
</body>
这里我们可以得出结论:JSX 背后的功能模块是什么React.createElement,该函数并没有做很多复杂的事情,基本上是在进行格式化数据的操作,执行到最后会 return 一个针对 ReactElement 的调用。
4. 总结
问题 | 回答 |
---|---|
JSX 的本质是什么,它和 JS 之间到底是什么关系? | JSX 的本质其实是React.createElement这个 JavaScript 调用的语法糖 |
为什么要用 JSX?不用会有什么后果? | JSX 语法糖允许前端开发者使用我们最为熟悉的类 HTML 标签语法来创建虚拟 DOM,在降低学习成本的同时,也提升了研发效率与研发体验。 |
JSX 背后的功能模块是什么,这个功能模块都做了哪些事情? | JSX 背后的功能模块是什么React.createElement,该函数并没有做很多复杂的事情,基本上是在进行格式化数据的操作,执行到最后会 return 一个针对 ReactElement 的调用 |
若对您有帮助,支持一下哦~
参考内容:
JSX
JSX 代码是如何“摇身一变”成为 DOM 的?