11.2 JSX 与编译:JSX 转换、Babel 插件、@jsx 注释

JSX 转换、Babel 插件、@jsx 注释

JSXBabelAST编译

原理

JSX(JavaScript XML)是一种 JavaScript 语法扩展,允许在 JavaScript 中编写类似 HTML 的结构。JSX 不是模板引擎,而是纯粹的语法糖,最终会被编译为普通的 JavaScript 函数调用。

JSX 的本质

以下 JSX 代码:

const element = (
  <h1 className="greeting">
    Hello, {name}!
  </h1>
);

经过 Babel 或 TypeScript 编译后,等价于:

const element = React.createElement(
  'h1',
  { className: 'greeting' },
  'Hello, ',
  name,
  '!'
);

React 17 引入了新的 JSX 转换(JSX Transform),不再需要显式导入 React

// React 17+ 自动运行时
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('h1', { className: 'greeting', children: 'Hello, world!' });

新转换的优势:

  • 无需在每个文件顶部 import React from 'react'
  • 生成的包体积略小(jsx 函数的签名更简洁)。
  • 支持通过 Babel 配置自定义 JSX 运行时(如 Preact、Emotion)。

JSX 编译的 AST 转换

Babel 处理 JSX 时,通过 @babel/plugin-transform-react-jsx 插件执行 AST 转换:

  1. 解析@babel/parser 将 JSX 语法解析为特殊的 AST 节点(JSXElementJSXAttributeJSXExpressionContainer 等)。
  2. 转换:访问者遍历 JSX 节点,将其替换为函数调用表达式。
  3. 生成:输出标准 JavaScript。

JSX 中的表达式插值 {} 在 AST 中被解析为 JSXExpressionContainer,其内部可以是任意 JavaScript 表达式。

关键编译规则

  • 标签名以小写字母开头:编译为字符串字面量(如 'div'),表示 HTML 原生元素。
  • 标签名以大写字母开头:编译为变量引用(如 MyComponent),表示自定义组件。
  • 展开属性{...props} 编译为 Object.assign 或对象展开。
  • 条件渲染{condition && <A />} 是惯用法,但注意 condition0 时会渲染 0
// 条件渲染陷阱
{count && <Badge />}
// 当 count === 0 时,渲染 0 而非不渲染
// 应改为:{count > 0 && <Badge />} 或 {count ? <Badge /> : null}

@jsx 注释与自定义运行时

在 Babel 配置不便修改的场景(如单文件测试),可通过文件顶部的注释指定 JSX 运行时:

/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from 'preact';

const vnode = <div>Hello</div>; // 编译为 h('div', null, 'Hello')

React 17+ 的自动运行时通过 /** @jsxImportSource preact */ 指定自定义来源。

用法

// JSX 中的 JavaScript 表达式
function UserCard({ user }) {
  return (
    <div className={`card ${user.vip ? 'vip' : ''}`}>
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name.toUpperCase()}</h2>
      <ul>
        {user.tags.map(tag => (
          <li key={tag.id}>{tag.name}</li>
        ))}
      </ul>
    </div>
  );
}

实践

列表渲染与 key 属性

JSX 列表必须提供稳定的 key 属性,帮助 React 识别哪些元素发生了变化。key 应在兄弟节点间唯一,推荐使用数据本身的唯一 ID,而非数组索引(除非列表是静态的)。

避免 JSX 中的复杂表达式

JSX 中嵌套过深的表达式会降低可读性。建议将复杂逻辑提取到组件外部或自定义 Hook 中:

// 不好
return (
  <div>
    {data.filter(x => x.active).map(x => x.name).join(', ')}
  </div>
);

// 好
const activeNames = data.filter(x => x.active).map(x => x.name).join(', ');
return <div>{activeNames}</div>;

陷阱

| 陷阱 | 现象 | 解决方案 | |------|------|---------| | class 代替 className | 控制台警告,样式不生效 | 使用 className;或使用 clsx/classnames 库 | | for 代替 htmlFor | 控制台警告 | 使用 htmlFor | | style 传入字符串 | 样式不生效 | style 必须传入对象 { color: 'red' } | | 0 && <Component /> | 渲染出 0 | 使用 count > 0 && 或三元表达式 | | 忘记 key 或使用索引 | 列表更新时状态错乱、性能下降 | 使用数据唯一 ID 作为 key |

测验

关联章节网络

当前章节
关联章节
交叉引用
后续延伸