JSX 是一种语法,并不是 React 中的内容,时下接入 JSX 语法的框架越来越多,但与之缘分最深的仍然是 React。本节来讲一下 React 是如何摇身一变成为 DOM 的。
我们平时在写React时会用 JSX 来描述组件的内容,例如下面的代码中,render 方法 return 的内容就是 JSX 代码。
jsx
class App extends React.Component {
render() {
return (
<div className="App">
<h1 className="title">I am the title</h1>
<p className="content">I am the content</p>
</div>
);
}
}
我们考虑以下三个问题:
- JSX 的本质是什么,它和 JS 之间到底是什么关系?
- React 为什么要用 JSX?
- JSX 是如何映射为 DOM 的?
这一节我们就将这三个问题一一解答。
1)JSX 的本质是什么?它和JS之间的到底是什么关系?
JSX 到底是什么,我们先来看看 React 官网给出的一段定义:
JSX 是 JavaScript 的一种语法扩展,它和模板语言很接近,但是它充分具备 JavaScript 的能力。
"语法扩展"这一点在理解上几乎不会产生歧义,不过"它充分具备 JavaScript 的能力"这句,却总让人摸不着头脑,JSX 和 JS 怎么看也不像是一路人啊?这就引出了"JSX 语法是如何在 JavaScript 中生效的"这个问题。
JSX 是 JavaScript 的扩展,而不是 JavaScript 的某个版本,因此浏览器并不会天然支持,那么 JSX 是如何在 JavaScript 中生效的呢?
React 官网是这样的解释的:
JSX 会被编译为 React.createElement(), React.createElement() 将返回一个叫作"React Element"的 JS 对象。
那么 JSX 如何转换成 React.createElement() 的呢?答案就是通过 babel 转换。
我们直接打开 babel playground 来写一段 JSX 代码看一下 babel 转换后的结果。
可以看到 JSX 代码都被转换成了 React.createElement 调用。
接下来我们总结一下来回答标题提到的两个问题。
JSX 是 JavaScript 的扩展,不是 JavaScipt 的某个版本,需要通过 Babel 进行转换成 JavaScript 代码。
JSX 会被 babel 转换为 React.CreateElement(...) 调用的形式,执行后返回的结果是一个对象。
2)React 为什么要用 JSX?
从上一节我们知道 JSX 等价于一次 React.createElement 调用,那么 React 官方为什么不直接引导我们用 React.createElement 来创建元素呢?
在实际功能效果一致的前提下,JSX 代码层次分明、嵌套关系清晰;而 React.createElement 代码则给人一种非常混乱的"杂糅感",这样的代码不仅读起来不友好,写起来也费劲。
JSX 语法糖允许前端开发者使用我们最为熟悉的类 HTML 标签语法来创建虚拟 DOM,在降低学习成本的同时,也提升了研发效率与研发体验。
3)JSX 是如何映射为 DOM 的?
我们知道 JSX 经过babel转换后会变成 React.createElement(...)
的形式,接下来我们就来一起探讨一下 React.createElement(...)
是如何工作的?
3.1 入参解读:创造一个元素需要知道哪些信息
我们先来看看方法的入参:
bash
export function createElement(type, config, children)
createElement 有 3 个入参,这 3 个入参囊括了 React 创建一个元素所需要知道的全部信息。
- type:用于标识节点的类型。它可以是类似"h1""div"这样的标准 HTML 标签字符串,也可以是 React 组件类型或 React fragment 类型。
- config:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中。
- children:子节点,如果有多个子节点,那么依次往后写。
举个例子:
jsx
<ul className="list">
<li key="1">1</li>
<li key="2">2</li>
</ul>
经过 Babel 转换后的形式为:
注意:从第三个入参开始往后,传入的参数都是 children
javascript
React.createElement("ul", {
// 传入属性键值对
className: "list"
}, React.createElement("li", {
key: "1"
}, "1"), React.createElement("li", {
key: "2"
}, "2"));
3.2 出参解读:初识虚拟DOM
下面的代码是 React.createElement(...) 调用的返回值格式。
注意:这是 fiber节点之前的每个节点的格式。
react
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;
};
举个例子
jsx
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"(准确地说,是虚拟 DOM 中的一个节点。
既然是"虚拟 DOM",那就意味着和渲染到页面上的真实 DOM 之间还有一些距离,这个"距离",就是由大家喜闻乐见的ReactDOM.render方法来填补的。
在每一个 React 项目的入口文件中,都少不了对 React.render 函数的调用。下面我简单介绍下 ReactDOM.render 方法的入参规则:
复制代码
jsx
ReactDOM.render(
// 需要渲染的元素(ReactElement)
element,
// 元素挂载的目标容器(一个真实DOM)
container,
// 回调函数,可选参数,可以用来处理渲染结束后的逻辑
[callback]
)
ReactDOM.render 方法可以接收 3 个参数,其中第二个参数就是一个真实的 DOM 节点 ,这个真实的 DOM 节点充当"容器"的角色,React 元素最终会被渲染到这个"容器"里面去。比如,示例中的 App 组件,它对应的 render 调用是这样的:
jsx
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
注意,这个真实 DOM 一定是确实存在的。比如,在 App 组件对应的 index.html 中,已经提前预置 了 id 为 root 的根节点:
jsx
<body>
<div id="root"></div>
</body>