React JSX 转换原理与 GSR 实现解析
本文是我在学习 React JSX 转换原理及其底层实现机制时的笔记与理解,主要涉及 GSR(即自定义的
createElement
实现)、Babel 编译机制、React 包结构及 JSX 的运行逻辑。通过梳理 JSX → JS → DOM 的全过程,深入理解 React 声明式编程的本质。
一、React 项目结构与包关系
在 React 的源码架构中,各个包之间分工明确:
react
包
提供与运行环境无关的通用方法和数据结构(例如 ReactElement 的定义与创建)。
可以理解为 React 的"核心逻辑层"。react-reconciler
包
负责协调(reconcile)虚拟 DOM 树与真实 DOM 的差异。
这是 React 更新机制的核心部分。react-dom
包
作为浏览器环境的实现层,将协调结果映射到实际的 DOM 操作上。shared
包
定义了通用类型与工具函数,供多个包复用。
这种拆分结构的优势在于:React 可以被用于浏览器、Native、甚至自定义渲染器中,而核心逻辑只需要维护一份。
二、JSX 是什么?为什么是 "XML" 语法糖?
1. JSX 并不是 HTML
很多初学者容易误会 JSX 是 HTML。
其实,JSX 只是 JavaScript 的一种语法扩展,让我们可以用更直观的方式声明 UI 结构。
例如:
ini
const element = <h1 className="title">Hello React</h1>;
在编译后,会被 Babel 转换为:
ini
const element = React.createElement("h1", { className: "title" }, "Hello React");
所以,JSX 最终都会变成 JavaScript 函数调用。
2. 为什么说是 "XML-like"?
JSX 采用了与 XML 类似的标签结构(如 <tag attr="value">children</tag>
),语法上接近 HTML,但并不等价于 HTML。
例如:
- JSX 中的
class
必须写成className
; - JSX 中可以嵌入任意 JS 表达式
{}
; - JSX 语义最终由 JS 决定,而非浏览器 DOM。
换句话说,JSX 是一种 "声明界面结构的 XML 语法糖" ,
它让我们可以像写 HTML 一样"声明"界面,但底层执行的仍是 JS 逻辑。
3. "语法糖"是什么意思?
"语法糖"(Syntactic Sugar)指的是让代码更易读、更易写的语法形式,但不会改变语言本身的功能。
举个例子:
css
<div>Hello</div>
和
csharp
React.createElement("div", null, "Hello")
两者最终效果一致,只是前者更"甜"一点(更直观),因此叫"语法糖"。
三、JSX 转换原理:编译时与运行时
JSX 的实现过程分为两个阶段:
1. 编译时(Babel 转换)
由 Babel 完成,将 JSX 转换为 JS 函数调用。
转换规则示例:
css
<Button color="blue">Click</Button>
➡️ 编译后变为:
css
React.createElement(Button, { color: "blue" }, "Click");
Babel 在这里扮演"语法翻译器"的角色。
2. 运行时(执行 GSR 或 React.createElement)
编译后的代码会调用运行时提供的 React.createElement
方法。
在自定义实现(如 GSR 转换)中,我们可以自定义该方法的行为。
示例实现:
csharp
function GSR(type, config, ...children) {
const props = { ...config };
if (children.length > 0) {
props.children = children.length === 1 ? children[0] : children;
}
return { type, key: props.key || null, ref: props.ref || null, props };
}
最终返回一个 ReactElement 对象:
yaml
{
type: 'div',
key: null,
ref: null,
props: { className: 'app', children: 'Hello React' }
}
这就是虚拟 DOM(Virtual DOM)的基本形态。
四、RedElement 与 RedSymbol 的设计
在 React 内部,虚拟 DOM(ReactElement)是以对象结构存在的。
项目中自定义实现的 RedElement 结构如下:
rust
{
$$typeof: Symbol('red.element'),
type,
key,
ref,
props
}
$$typeof
:用于区分对象类型,防止滥用;type
:元素类型(如'div'
或组件);key
:用于 Diff;ref
:用于访问 DOM 或组件;props
:组件属性。
为了确保每个元素的唯一性,引入了 RedSymbol:
- 如果环境支持
Symbol
,就使用Symbol('red.element')
; - 否则使用一个数字常量代替。
这样可以保证每个 ReactElement 在调试和识别时都是安全且唯一的。
五、React.createElement 的底层逻辑
React 的 createElement
实现与 GSR 几乎一致。
伪代码如下:
ini
function createElement(type, config, ...children) {
const props = {};
let key = null;
let ref = null;
if (config) {
key = config.key ?? null;
ref = config.ref ?? null;
for (const prop in config) {
if (prop !== 'key' && prop !== 'ref') {
props[prop] = config[prop];
}
}
}
if (children.length > 0) {
props.children = children.length === 1 ? children[0] : children;
}
return ReactElement(type, key, ref, props);
}
这一过程说明:
- JSX 的"声明式"结构只是表象;
- React 内部依然是命令式 JS 函数调用;
- React 在运行时会再通过 reconciler 将这些虚拟节点映射为真实 DOM 操作。
六、React 17 之后 JSX 转换变化
React 17 之后引入了新的 JSX 转换方案。
即不再强制要求显式导入 React:
javascript
// React 17 之前
import React from 'react';
const element = <div />;
// React 17 之后
const element = <div />; // 仍可正常工作
Babel 会自动引入新的运行时辅助函数 jsx
或 jsxs
,例如:
javascript
import { jsx as _jsx } from 'react/jsx-runtime';
_jsx("div", { className: "box" });
这使得编译输出更简洁、性能更高。
七、总结与理解提升
- JSX 是一种 声明式的语法糖,让我们以接近 HTML 的方式描述组件结构;
- 它本质是 JavaScript 调用 React.createElement 的语法封装;
- 编译阶段由 Babel 转换,运行阶段由 React 运行时(或自定义 GSR)解析;
- React 底层依然是命令式的,通过协调器将虚拟 DOM 同步到真实 DOM;
- React 17 之后引入了新的 JSX 转换机制,使开发体验更自然。
结语
理解 JSX 转换原理,就能更清楚地认识到 React 的核心哲学:
"表面是声明式的 JSX,实质是命令式的 JavaScript。"
这也是 React 高效、灵活的根本原因------它让开发者用声明的方式思考,但底层通过命令式逻辑精确地控制 DOM。