React JSX
认识 JSX
React 18 以后,JSX 会被编译成 jsx() 或 jsxs() 函数调用
示例 1:单个子节点
const el = <h1 className="title">Hello</h1>;
编译结果:
import { jsx } from "react/jsx-runtime";
const el = jsx("h1", { className: "title", children: "Hello" });
示例 2:多个子节点
const el = (
<div>
<span>Hello</span>
<span>World</span>
</div>
);
编译结果:
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const el = /*#__PURE__*/_jsxs("div", {
children: [/*#__PURE__*/_jsx("span", {
children: "Hello"
}), /*#__PURE__*/_jsx("span", {
children: "World"
})]
});
返回的是一个 ReactElement 对象:
{
$$typeof: Symbol(react.element), // React 内部标识
type: "div", // HTML 标签或组件
key: null, // JSX key 属性
ref: null, // JSX ref 属性
props: {
children: [
{
$$typeof: Symbol(react.element),
type: "span",
key: null,
ref: null,
props: { children: "Hello" },
_owner: null
},
{
$$typeof: Symbol(react.element),
type: "span",
key: null,
ref: null,
props: { children: "World" },
_owner: null
}
]
},
_owner: null // Fiber 所有者(编译期为 null)
}
🚀 为什么使用 jsx/jsxs 替代 React.createElement?
优势:
- 性能更高:避免额外判断 children;
- Tree-shaking 更好:不依赖 React 实例,减少打包体积;
- 更清晰分离职责:UI 创建逻辑与运行时解耦。
最终,在调和阶段,上述 返回的是一个 ReactElement 对象: 对象的每一个子节点都会形成一个与之对应的 fiber 对象,然后通过 sibling、return、child 将每一个 fiber 对象联系起来。
本质上来说,JSX 只是为 ReactElement()
提供的一种语法糖。如果在 JSX 中往 DOM 元素中传入自定义属性,React 是不会渲染的(因为 React 无法识别)。
React 针对不同 ReactElement 对象会产生不同 tag (种类) 的fiber 对象。首先,来看一下 tag 与 element 的对应关系:
export const FunctionComponent = 0; // 函数组件
export const ClassComponent = 1; // 类组件
export const HostRoot = 3; // Root Fiber 可以理解为根元素
export const HostPortal = 4; // 对应 ReactDOM.createPortal 产生的 Portal
export const HostComponent = 5; // dom 元素 比如 <div>
export const HostText = 6; // 文本节点
export const Fragment = 7; // 对应 <React.Fragment>
export const Mode = 8; // 对应 <React.StrictMode>
export const ContextConsumer = 9; // 对应 <Context.Consumer>
export const ContextProvider = 10; // 对应 <Context.Provider>
export const ForwardRef = 11; // 对应 React.ForwardRef
export const Profiler = 12; // 对应 <Profiler/ >
export const SuspenseComponent = 13; // 对应 <Suspense>
export const MemoComponent = 14; // 对应 React.memo 返回的组件
// 目前到31
fiber 对应关系:
- child: 一个由父级 fiber 指向子级 fiber 的指针。
- return:一个子级 fiber 指向父级 fiber 的指针。
- sibling: 一个 fiber 指向下一个兄弟 fiber 的指针。
对于上述在 jsx 中写的 map 数组结构的子节点,外层会被加上 fragment;map 返回数组结构,作为 fragment 的子节点。
Babel 解析 JSX 流程
Babel 解析 JSX 的流程是将 JSX 语法转换为 JavaScript 代码的过程,核心目的是 把 <div>Hello</div>
这样的 JSX 转成 jsx("div", { children: "Hello" }) 这样的函数调用。
babel 插件
JSX 语法实现来源于这两个 babel 插件:
- @babel/plugin-syntax-jsx : 使用这个插件,能够让 Babel 有效的解析 JSX 语法。
- @babel/plugin-transform-react-jsx :这个插件内部调用了 @babel/plugin-syntax-jsx,可以把 React JSX 转化成 JS 能够识别的格式。
我们可以从宏观角度和底层插件角度分两层看这个过程。
✅ 一图看懂 Babel 解析 JSX 的流程
JSX 源码
↓
Babel 解析 (Parse) ——→ 生成 AST
↓
Babel 插件转换 (Transform) ——→ JSX → JS
↓
Babel 生成代码 (Generate)
↓
最终 JS 输出(如 jsx(...))
🧠 Babel 解析 JSX 的详细步骤
1. Parsing 阶段(构建 AST)
Babel 使用 @babel/parser 将源码解析为 AST(抽象语法树)。
这一步只是识别出
<div>Hello</div>
是 JSX 节点。
📌 JSX 会被 Babel 识别为 JSXElement, JSXOpeningElement, JSXText 等节点类型。
例如:
const element = <h1>Hello</h1>;
会被解析为类似:
{
"type": "VariableDeclaration",
"declarations": [{
"id": { "name": "element" },
"init": {
"type": "JSXElement",
"openingElement": {
"name": { "type": "JSXIdentifier", "name": "h1" }
},
"children": [{
"type": "JSXText",
"value": "Hello"
}]
}
}]
}
2. Transformation 阶段(插件转换)
🎯 使用的 Babel 插件:
- @babel/plugin-transform-react-jsx
- 或开启 @babel/preset-react
这个插件会将 JSXElement 类型节点转换为:
- React 17 前:React.createElement(...)
- React 17 后默认使用:jsx(...)、jsxs(...)
3.Code Generation(生成代码)
- Babel 使用 @babel/generator 将转换后的 AST 重新生成 JavaScript 代码字符串。
- 输出成实际的 JavaScript 文件内容。
Automatic Runtime
新版本 React 已经不需要引入 createElement ,这种模式来源于 Automatic Runtime
:
// 业务代码
function Index(){
return <div>
<h1>hello,world</h1>
<span>let us learn React</span>
</div>
}
// 编译结果
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
function Index() {
return _jsxs("div", {
children: [
_jsx("h1", {
children: "hello,world"
}),
_jsx("span", {
children:"let us learn React" ,
}),
],
});
}
plugin-syntax-jsx
已经向文件中提前注入了 _jsxRuntime api
。不过这种模式下需要我们在 .babelrc
设置 runtime: automatic 。
"presets": [
["@babel/preset-react",{
"runtime": "automatic"
}]
],
Classic Runtime
在经典模式下,使用 JSX 的文件需要引入 React ,不然就会报错。
// 业务代码
import React from 'react'
function Index(){
return <div>
<h1>hello,world</h1>
<span>let us learn React</span>
</div>
}
// 编译后文件
import React from 'react'
function Index(){
return React.createElement(
"div",
null,
React.createElement("h1", null,"hello,world"),
React.createElement("span", null, "let us learn React")
);
}
Babel 编译过程
const fs = require('fs')
const babel = require("@babel/core")
/* 第一步:模拟读取文件内容。 */
fs.readFile('./element.js',(e,data)=>{
const code = data.toString('utf-8')
/* 第二步:转换 jsx 文件 */
const result = babel.transformSync(code, {
plugins: ["@babel/plugin-transform-react-jsx"],
});
/* 第三步:模拟重新写入内容。 */
fs.writeFile('./element.js',result.code,function(){})
})
// 转译后 成功转成 React.createElement 形式
import React from 'react';
function TestComponent() {
return /*#__PURE__*/React.createElement("p", null, " hello,React ");
}
function Index() {
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", null, "\u6A21\u62DF babel \u5904\u7406 jsx \u6D41\u7A0B\u3002"), /*#__PURE__*/React.createElement(TestComponent, null));
}
export default Index;
问题
Q1. 老版本的 React 中,为什么写 jsx 的文件要默认引入 React?
import React from 'react'
function Index(){
return <div>hello,world</div>
}
因为 jsx 在被 babel 编译后,写的 jsx 会变成 React.createElement 形式,所以需要引入 React,防止找不到 React 引起报错。@babel/plugin-syntax-jsx ,在编译的过程中注入 _jsxRuntime api
,使得新版本 React 已经不需要引入 createElement。
本质上来说 JSX 是 React.createElement(component, props, ...children)
方法的语法糖。在 React 17
之前,如果使用了JSX,其实就是在使用 React, babel 会把组件转换为 CreateElement 形式。在 React 17
之后,就不再需要引入,因为 babel 已经可以帮我们自动引入 react。
JSX
与 JS
的区别:
JS
可以被打包工具直接编译,不需要额外转换,jsx
需要通过babel
编译,它是React.createElement
的语法糖,使用jsx
等价于React.createElement
jsx
是js
的语法扩展,允许在html
中写JS
。JS
是原生写法,需要通过script
标签引入
Q2. React.createElement
和 React.cloneElement
到底有什么区别?
一个是用来创建 element 。另一个是用来修改 element,并返回一个新的 React.element
对象。在React
中,所有JSX
在运行时的返回结果(即React.createElement()
的返回值)都是React Element
。
Q3. jsx 做了什么?
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
};
export function createElement(type, config, ...children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
// Currently, key can be spread in as a prop. This causes a potential
// issue if key is also explicitly declared (ie. <div {...props} key="Hi" />
// or <div key="Hi" {...props} /> ). We want to deprecate key spread,
// but as an intermediary step, we will use jsxDEV for everything except
// <div {...props} key="Hi" />, because we aren't currently able to tell if
// key is explicitly declared to be undefined or not.
if (maybeKey !== undefined) {
if (__DEV__) {
checkKeyStringCoercion(maybeKey);
}
key = '' + maybeKey;
}
if (hasValidKey(config)) {
if (__DEV__) {
checkKeyStringCoercion(config.key);
}
key = '' + config.key;
}
if (hasValidRef(config)) {
ref = config.ref;
}
// Remaining properties are added to a new props object
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(
type,
key,
ref,
undefined,
undefined,
ReactCurrentOwner.current,
props,
);
}
jsx 函数主要是做了一个预处理,然后将处理好的数据传入 ReactElement 函数中。ReactElement 函数,代码精简后如下:
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
return element;
};
Q4. JSX 安全性
字符串转义
React 会将所有要显示到 DOM 的字符串转义,防止 XSS。所以,如果 JSX 中含有转义后的实体字符,比如 ©
(©),则最后 DOM 中不会正确显示,因为 React 自动把 ©
中的特殊字符转义了。
有几种解决方案:
- 直接使用 UTF-8 字符
- 使用对应字符的 Unicode 编码查询编码
- 使用数组组装
<div>{['cc ', <span>©</span>, ' 2015']}</div>
- 直接插入原始的 HTML
此外,React 提供了 dangerouslySetInnerHTML
属性。正如其名,它的作用就是避免 React 转义字符,在确定必要的情况下可以使用它。
<div dangerouslySetInnerHTML={{ __html: 'cc © 2015' }} />
避免 XSS 注入攻击
React 中 JSX 能够帮我们自动防护部分 XSS 攻击,譬如我们常见的需要将用户输入的内容再呈现出来:
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;
在标准的 HTML 中,如果我们不对用户输入作任何的过滤,那么当用户输入 <script>alert(1)<script/>
这样的可执行代码之后,就存在被 XSS 攻击的危险。而 React 在实际渲染之前会帮我们自动过滤掉嵌入在 JSX 中的危险代码,将所有的输入进行编码,保证其为纯字符串之后再进行渲染。不过这种安全过滤有时候也会对我们造成不便,譬如如果我们需要使用 ©
这样的实体字符时,React 会自动将其转移最后导致无法正确渲染,上面提及的字符串转义就起到作用了。
function createMarkup() { return { __html: 'First · Second' };}
function MyComponent() { return <div dangerouslySetInnerHTML={createMarkup()} />;}
Q5. JSX 与 Fiber 节点间的关系
JSX
是一种描述当前组件内容的数据结构,他不包含组件schedule、reconcile、render所需的相关信息。
比如如下信息就不包括在JSX
中:
- 组件在更新中的
优先级
- 组件的
state
- 组件被打上的用于Renderer的
标记
这些内容都包含在Fiber节点
中。
所以,在组件mount
时,Reconciler
根据JSX
描述的组件内容生成组件对应的Fiber节点
。在update
时,Reconciler
将JSX
与Fiber节点
保存的数据对比,生成组件对应的Fiber节点
,并根据对比结果为Fiber节点
打上标记
。