在 React 项目中,我们会使用 JSX 语法。简单来说,JSX 可以让你在 JavaScript 代码中编写类似 HTML(实际上是 XML)结构的代码,其本质是简化组件和元素结构的声明方式。
JSX 转译原理
JavaScript 本身不支持 JSX 语法,从我们编写的 React 代码到最终的运行产物,需要经历转译。以 Babel 为例,转译主要经历三个阶段:解析(parse) 、遍历(traverse)和生成(generate) 。针对 JSX 语法,Babel 提供专门的插件 @babel/plugin-transform-react-jsx
来处理转换(被包含在预设 @babel/preset-react
中)。
jsx
// 转换前
function App() {
return <div>Hello World</div>;
}
// React 17 之前(经典转换)
// 注意: 需要手动导入 React
// import React from 'react';
// 转换后
function App() {
return React.createElement("div", null, "Hello World");
}
// React 17 及之后(新的 JSX 转换)
// 注意:不需要显式导入 React,由转译器(不是编译器?)自动导入所需的函数
// 转换后
import { jsx as _jsx } from "react/jsx-runtime";
function App() {
return _jsx("div", { children: "Hello World" });
}
Babel 在处理 JSX 时,会先将其解析成抽象语法树(AST),然后遍历时通过访问者模式修改 AST 节点,最终生成 JavaScript 代码。下面是 @babel/plugin-transform-react-jsx
插件的核心实现逻辑:
js
module.exports = function (babel) {
const { types: t } = babel;
return {
visitor: {
JSXElement(path) {
// 获取元素类型
const openingElement = path.node.openingElement;
const tagName = openingElement.name.name;
// 处理属性
const attributes = openingElement.attributes.map((attr) => {
// 转换 JSX 属性为对象属性
// ...
});
// 处理子元素
const children = path.node.children.map((child) => {
// 转换子元素
// ...
});
// 创建 React.createElement 调用
const callExpression = t.callExpression(
t.memberExpression(
t.identifier("React"),
t.identifier("createElement"),
),
[
t.stringLiteral(tagName),
t.objectExpression(attributes),
...children,
],
);
// 替换原 JSX 节点
path.replaceWith(callExpression);
},
},
};
};
两种 createElement 的内部实现
React.createElement
当 JSX 被转换成 React.createElement()
调用后,该函数会创建一个 React 元素(React Element):
js
function createElement(type, config, ...children) {
// 提取特殊的 props: key, ref
let key = null;
let ref = null;
if (config != null) {
if (config.key !== undefined) key = "" + config.key;
if (config.ref !== undefined) ref = config.ref;
}
// 处理 props
const props = {};
// 复制 config 中的属性到 props
for (let propName in config) {
if (propName !== "key" && propName !== "ref") {
props[propName] = config[propName];
}
}
// 处理 children
if (children.length === 1) {
props.children = children[0];
} else if (children.length > 1) {
props.children = children;
}
// 创建 React 元素
const element = {
$$typeof: Symbol.for("react.element"),
type,
key,
ref,
props,
_owner: null, // React 内部使用,用于跟踪创建该元素的组件,对调试和警告信息有帮助
};
return element;
}
:::color4 React 元素是描述 UI 的不可变对象,包含以下主要属性:
- type: 元素的类型(字符串表示 DOM 元素,函数或类表示组件)
- props: 传递给元素的属性
- key: 用于在列表中标识元素,便于后续 Diff 算法识别
- ref: 引用元素的 DOM 节点或组件实例
- $$typeof: 用于标识这是一个 React 元素的符号,因为 Symbol 不能被 JSON 序列化,所以可以防止 XSS 攻击(服务器的 JSON 数据无法伪造有效的 React 元素)
:::
React 17 中的 JSX Runtime
React 17 引入新的 JSX 转换方式,使用 jsx-runtime
包中的函数。这个包提供两个主要函数:jsx
和 jsxs
,后者专门用于处理有多个子元素的情况:
js
// react/jsx-runtime 简化实现
export function jsx(type, config, maybeKey) {
let key = null;
let ref = null;
if (maybeKey !== undefined) {
key = "" + maybeKey;
} else if (config && config.key !== undefined) {
key = "" + config.key;
}
if (config && config.ref !== undefined) {
ref = config.ref;
}
// 创建 props 对象
const props = {};
for (let propName in config) {
if (propName !== "key" && propName !== "ref") {
props[propName] = config[propName];
}
}
return {
$$typeof: Symbol.for("react.element"),
type,
key,
ref,
props,
_owner: null,
};
}
// 处理有多个子元素的情况
export function jsxs(type, config, maybeKey) {
// 实际实现与 jsx 基本相同,但针对多子元素场景有优化
return jsx(type, config, maybeKey);
}
浅析
- 为什么需要 JSX 语法 :JSX 提供一种声明式的语法,使组件的结构和行为更直观,同时保持 JavaScript 的全部能力。相比于手动调用
createElement
函数,JSX 使代码更易读、更接近最终渲染的 UI 结构。 - 为什么 babel 转译不直接生成 React Element:保持 API 的稳定性和灵活性,同时允许内部实现变化。这样 React 团队可以在不破坏现有代码的情况下修改元素的创建过程、添加新特性或进行性能优化。
- 为什么改变了导入方式:新的 JSX 转换消除了不必要的导入并优化函数调用,可以带来轻微的性能改进和更小的包大小(通过自动导入特定的函数而非整个 React 包,可以更好地支持打包工具的优化)。
小结
- JSX 本质上是一种语法糖,它允许在 JavaScript 中编写类似 HTML 的代码,简化 UI 结构(Schema)的声明(其他框架也有应用场景)。
- JSX 在编译时会被转换函数调用(React.createElement 或 jsx/jsxs),运行时生成 React Element。
- React 17 引入的新 JSX 转换简化了开发体验,不再需要手动导入 React,但是无论使用哪种转换方式,React 最终生成的元素结构基本相同。