Skip to content

React JSX

认识 JSX

React 18 以后,JSX 会被编译成 jsx() 或 jsxs() 函数调用

示例 1:单个子节点

jsx
const el = <h1 className="title">Hello</h1>;

编译结果:

js
import { jsx } from "react/jsx-runtime";
const el = jsx("h1", { className: "title", children: "Hello" });

示例 2:多个子节点

jsx
const el = (
  <div>
    <span>Hello</span>
    <span>World</span>
  </div>
);

编译结果:

js
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 对象:

js
{
  $$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 的对应关系:

js
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 的流程

scss
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 等节点类型。

例如:

jsx
const element = <h1>Hello</h1>;

会被解析为类似:

json
{
  "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:

js
// 业务代码
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 。

js
"presets": [    
    ["@babel/preset-react",{
    "runtime": "automatic"
    }]     
],

Classic Runtime

在经典模式下,使用 JSX 的文件需要引入 React ,不然就会报错。

js
// 业务代码
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 编译过程

js
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?

js
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。

JSXJS 的区别:

  1. JS 可以被打包工具直接编译,不需要额外转换,jsx需要通过 babel 编译,它是React.createElement 的语法糖,使用 jsx等价于React.createElement
  2. jsxjs 的语法扩展,允许在 html中写 JSJS 是原生写法,需要通过 script 标签引入

Q2. React.createElementReact.cloneElement 到底有什么区别?

一个是用来创建 element 。另一个是用来修改 element,并返回一个新的 React.element 对象。在React中,所有JSX在运行时的返回结果(即React.createElement()的返回值)都是React Element

Q3. jsx 做了什么?

js
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 函数,代码精简后如下:

javascript
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 转义字符,在确定必要的情况下可以使用它。

jsx
<div dangerouslySetInnerHTML={{ __html: 'cc &copy; 2015' }} />

避免 XSS 注入攻击

React 中 JSX 能够帮我们自动防护部分 XSS 攻击,譬如我们常见的需要将用户输入的内容再呈现出来:

jsx
const title = response.potentiallyMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;

在标准的 HTML 中,如果我们不对用户输入作任何的过滤,那么当用户输入 <script>alert(1)<script/> 这样的可执行代码之后,就存在被 XSS 攻击的危险。而 React 在实际渲染之前会帮我们自动过滤掉嵌入在 JSX 中的危险代码,将所有的输入进行编码,保证其为纯字符串之后再进行渲染。不过这种安全过滤有时候也会对我们造成不便,譬如如果我们需要使用 © 这样的实体字符时,React 会自动将其转移最后导致无法正确渲染,上面提及的字符串转义就起到作用了。

jsx
function createMarkup() {  return { __html: 'First &middot; Second' };}
function MyComponent() {  return <div dangerouslySetInnerHTML={createMarkup()} />;}

Q5. JSX 与 Fiber 节点间的关系

JSX是一种描述当前组件内容的数据结构,他不包含组件schedulereconcilerender所需的相关信息。

比如如下信息就不包括在JSX中:

  • 组件在更新中的优先级
  • 组件的state
  • 组件被打上的用于Renderer标记

这些内容都包含在Fiber节点中。

所以,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。在update时,ReconcilerJSXFiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记