相信大家接触 React 最开始,肯定会对 JSX 感到非常好奇。不得不说在 React 推广初期,JSX 也是其能够吸引很多用户的原因。JSX 其实不是什么神奇的东西,他只是简化了一个特定的函数的调用方式,他的目的无非就是:让代码更易于书写和阅读。所以我们学习 JSX 不要神化他,同时也要知晓其到底是什么东西。
JSX 的语法定义
JSX 是 React 官方推荐在项目中使用的语法,React 对于 JSX 定义其实非常的简单:
JSX 只是对于
React.createElement(component, props, ...children)
的语法糖
JSX 看起来非常类似于 HTML,这是特意而为之的,因为 HTML 节点是 Web 页面的基本元素,虽然我们是通过 JS 来实现我们的应用,但实际上我们写的 JS 最终目的仍然是操作 HTML 节点。如果你使用过 jQuery,YUI 这类的工具类库,你会发现你其实经常要直接操作某个/某些 DOM 节点;如果你写过 angular/backbone 这类类库,你会发现你需要写一些类 HTML 模板;所以我们进行 Web 开发是无法避免和 HTML 打交道的,在 React 中同样如此。
而 React 因为引入了虚拟 DOM,我们虽然最终仍然要更新 DOM,但是最终这一步并不需要我们来做,React 会帮助我们完成。取而代之的是我们要创建一系列虚拟 DOM 节点,或者称之为:React 节点(ReactElement)。这类节点不再是直接的 HTML 节点,而是 React 定义的 JS 对象:
type ReactElement = {
type: T;
props: P;
key: Key | null;
};
我们需要通过React.createElement
函数来创建这类节点,而这个函数如何调用?自然就是上面提到的React.createElement(component, props, ...children)
。
<Component id="component">
<span>Hello</span>
</Component>
这是第一个参数,代表着组件的类型。
这些类似 HTML attribute 的写法,会被转换成 key value 形式的对象,这些属性被称为props。
写在组件标签中间的,会被编译成...children
,也就代表着他们是Component
的孩子节点。因为可以有多个孩子节点,每个节点都会直接作为createElement
的后续参数传入,所以是...children
。
学习 JSX 的重点就是,不要把他们看作是 HTML 节点,而是 JS 函数调用,这回让你对于 JSX 的理解上升一整个档次。在你习惯这样思考之后,你再看到 JSX 代码你脑子里面都会直接反射出他的 JS 代码,这时候你就已经如火纯青了。
组件类型
React 的组件类型,其实也就是React.createElement
函数的第一个参数,是的从这一刻开始我已经开始锻炼你以 JS 函数的方式来思考 JSX 代码的能力了。那么这个参数可以是什么类型呢?整体上其实就分为两种:
- 字符串
- 自定义
- 内置组件
字符串
字符串指代的是原生的 HTML 节点,就跟你写 HTML 一样,div/span/p
等。如果传给React.createElement
第一个参数是字符串,React 会知道后续要按照 DOM 节点的方式去处理,并且最终会渲染到页面上。而对于自定义类型,则是按照我们普遍认知的组件的方式进行处理。
原生 Dom 节点必须要用小些字母开头,JSX 编译器根据首字母是小写还是大写来进行编译:
<div>Hello</div>
<Div>Hello</Div>
对于上面两种情况,JSX 的编译器会产出不同的结果:
React.createElement('div', {}, 'Hello');
React.createElement(Div, {}, 'Hello');
对于第二个情况,我们知道Div
肯定是一个变量,你需要在作用域中有定义这个变量,不然这个代码机会报错。所以首字母的大小写是 JSX 编译结果的一个非常重要的指标,请务必牢记。
Note
对于自定义组件,你的组件变量必须要在作用域中存在,比如:
const element = <Div>Hello</Div>;
如果你的文件中只有这一行代码,那么他大概率是要报错的,因为找不到Div
这个变量,就像你写const a = b + 1
但是你并没有定义b
的时候一样。
自定义
而自定义又可以分为:
- 函数组件
- 类组件
从能力上讲,他们没有本质的区别,他们都是用来渲染节点片段的。但是他们的定义方式上有很多的区别,最大的区别就在与函数组件可以使用 hooks,而类组件则多了生命周期的一些概念。自从 hooks 出来之后,函数组件的重要性得到了无限的提高,从官方的角度上来讲 hooks 就是未来,所以类组件其实已经淡出历史舞台了,我们也会以函数组件为核心来进行后续的学习,React 的新官网的内容也是优先以 hooks 为主的。
内置组件
内置组件也就是 React 内置的一些组件,虽然他们也可以作为组件传递给React.createElement
,但是他们和我们认知的组件还是有很大差距的。本质上来说他们并不渲染什么,他们更多是一个标志,React 内置的组件有如下:
- Fragment
- StrictMode
- Suspense
- Profiler
- Lazy(
React.lazy
的返回值)
以上这些组件都只是一个简单的Symbol
,他们并没有任何的实现,也没有 render 函数,就只是一个简单的标志符。在 React 的定义中他们被称为ExoticComponent
。
为什么一个Symbol
能称为一个组件呢?要理解这个需要你对 React 的渲染方式进行理解,在这里不多说,因为太过复杂,未来在讲解 React 的高级内容时会进行讲解。现在你只需要理解一点,组件本身对于 React 来说只是一个标志符,他可以根据组件类型来判断是否需要执行组件的render
函数,对于内置组件,React 并不需要执行什么,只需要知道他们的含义,以及如何处理他们的 props 即可。
表达式
在 JSX 中可以非常方便地使用 JS 表达式,表达式让 JSX 变得非常灵活,几乎所有你写 JS 能实现的功能都可以在 JSX 中实现。那么为什么 JSX 中可以如此方便地使用表达式呢?还是因为 JSX 仅仅只是函数调用的语法糖,他并没有改变或者扩展 JS 的语法。
React.createElement(Component, {key: '123'}, 'Children');
对于这样一段 JS 代码,你自然是可以非常方便的在其中使用表达式的,比如你的key
可以是动态生成的,{key: i+1}
。
在 JSX 中要使用表达式,需要使用{}
进行标识,在{}
中的代码,编译器就知道他是一个表达式。上面的例子翻译成 JSX 如下:
<Component key={i + 1}>Children</Component>
直接在children
使用字面量会编译成字符串,上面例子中的Children
写成{'Children'}
是一样的效果。
Children
官方文档已经非常详细。
children
也是 props 的一部分,这一点大家也是知道,但是我们上面讲到React.createElement
函数的时候我们可以看到,children
和 props 并不是一起传递的,那么为什么children
又是 props 的一部分呢?
要理解这一点其实也很简单,就是看React.createElement
的返回:
interface ReactElement<
P = any,
T extends string | JSXElementConstructor<any> =
| string
| JSXElementConstructor<any>
> {
type: T;
props: P;
key: Key | null;
}
可以看到这里面并没有children
,这是因为在执行函数的过程中,React 把children
合并到了 props 中。

在视屏中我会陪你一起阅读这部分 React 的源码,让你知道React.createElement
到底做了什么神奇的事情。
小结
理解 JSX 最重要就是知道他编译到 JS 之后是什么样的,只要你能快速切换 JSX 和 JS 的代码,那么未来你遇到 JSX 的问题就能非常快速地解决。