陌上人如玉
公子世无双

1.1 JSX 语法与本质

1.1 JSX 语法与本质

一句话:JSX 是 React.createElement()(React 17+ 是 jsx() 运行时)的语法糖,编译后返回一个普通的 JavaScript 对象(React Element);它既不是字符串也不是 HTML,因此有自己的一套约束。

一、JSX 是什么

// 你写的
const element = <h1 className="title">Hello, {name}!</h1>;

// React 17 之前(经典运行时)编译为:
const element = React.createElement(
  'h1',
  { className: 'title' },
  'Hello, ',
  name,
  '!'
);

// React 17+(自动运行时)编译为:
import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime';
const element = _jsx('h1', { className: 'title', children: ['Hello, ', name, '!'] });

编译产物就是一个对象:

// 上面 element 的真实结构(简化)
{
  $$typeof: Symbol(react.element),   // ⭐ 标识这是 React Element(防 XSS 的结构)
  type: 'h1',
  key: null,
  ref: null,
  props: {
    className: 'title',
    children: ['Hello, ', name, '!'],
  },
}

为什么要用 $$typeof:早期 React 允许从服务端返回 JSON.parse 出来的对象直接渲染,存在被注入「伪造 Element」的风险(比如注入一个 type: 'script')。用 Symbol 标识后,JSON 无法伪造 Symbol,从而堵住这个 XSS 途径。

自动运行时(Automatic Runtime)带来的变化:

// React 17 之前:每个文件都要显式导入 React
import React from 'react';
const el = <div />;

// React 17+:不需要导入 React,编译器自动注入 jsx 运行时
const el = <div />;
维度 经典运行时 自动运行时(React 17+)
是否需要 import React ✅ 必须 ❌ 不需要(除非用到 React.xxx)
编译产物 React.createElement jsx / jsxs(从 react/jsx-runtime)
配置 默认 jsx: 'react-jsx'(Vite/TS 默认)
性能 略差(运行时判断参数) 略优(jsx/jsxs 分开处理静态子节点)

二、表达式嵌入:{ } 里能写什么

const name = '张三';
const list = [1, 2, 3];

// ✅ 可以写:任何「表达式」
<div>
  {name}                                  {/* 变量 */}
  {1 + 2}                                 {/* 运算 */}
  {name.toUpperCase()}                    {/* 函数调用 */}
  {list.map((x) => <li key={x}>{x}</li>)} {/* 数组 */}
  {flag ? <A /> : <B />}                  {/* 三元 */}
  {user?.name}                            {/* 可选链 */}
  {list.length > 0 && <List />}           {/* 逻辑与 */}
</div>

// ❌ 不能写:语句
<div>
  {if (flag) { return <A /> }}            {/* 语法错误 */}
  {for (let i = 0; i < 3; i++) {}}        {/* 语法错误 */}
  {const x = 1}                           {/* 语法错误 */}
</div>
// 需要写逻辑时,把计算提到 JSX 外面
function Component({ list, flag }) {
  const filtered = list.filter((x) => x.active);      // ✅ 在函数体里算
  const total = list.reduce((a, b) => a + b.price, 0);

  // 或者用 IIFE(不推荐,可读性差)
  // {(() => { if (flag) return <A />; return <B />; })()}

  return (
    <div>
      {filtered.map((x) => <Item key={x.id} {...x} />)}
      <span>{total}</span>
    </div>
  );
}

三种值的渲染规则:

值 渲染结果
字符串、数字 直接显示
null / undefined / false / true 什么都不渲染(可用于条件渲染)
数组 逐个渲染(每项需要 key)
普通对象 ❌ 报错(Objects are not valid as a React child)
// ⚠️ 常见的「对象渲染报错」
const user = { name: '张三' };
return <div>{user}</div>;             // ❌ 报错
return <div>{user.name}</div>;         // ✅
return <div>{JSON.stringify(user)}</div>;   // ✅(如果只是想展示)

三、条件渲染的写法

// ① 三元(最常用,适合二选一)
{isLogin ? <UserPanel /> : <LoginButton />}

// ② 逻辑与(只关心「有或没有」)
{list.length > 0 && <List data={list} />}

// ③ 提前 return(组件级条件渲染,可读性最好)
function Page({ loading, data }) {
  if (loading) return <Skeleton />;
  if (!data) return <Empty />;
  return <Content data={data} />;
}

// ④ 变量提取
let content;
if (loading) content = <Skeleton />;
else if (error) content = <ErrorView />;
else content = <Content />;
return <div>{content}</div>;

// ⑤ 映射表(多分支场景最清晰)
const views = {
  loading: <Skeleton />,
  error: <ErrorView />,
  empty: <Empty />,
  success: <Content data={data} />,
};
return <div>{views[status]}</div>;

⚠️ && 的 0 陷阱(最高频的 JSX 坑):

// ❌ count 为 0 时会渲染出「0」
{count && <List count={count} />}
// 渲染结果:<div>0</div>

// 原因:0 是「假值但可渲染」的值 —— React 会把它当作子节点渲染出来
// (而 false / null / undefined / true 会被忽略)

// ✅ 四种修复
{count > 0 && <List count={count} />}
{!!count && <List count={count} />}
{Boolean(count) && <List count={count} />}
{count ? <List count={count} /> : null}
值 {value && <X />} 的结果
0 渲染出 0 ⚠️
''(空字符串) 渲染出空(不可见,但仍是文本节点)
NaN 渲染出 NaN ⚠️
null / undefined / false / true 什么都不渲染 ✅
[](空数组) 什么都不渲染 ✅

四、列表渲染与 key

// 基本写法
{list.map((item) => (
  <Item key={item.id} data={item} />          // ⭐ key 必须在 map 返回的最外层元素上
))}

// ❌ 常见错误:key 加在了内部元素上
{list.map((item) => (
  <div>
    <Item key={item.id} data={item} />        {/* ⚠️ 警告 + 失效 */}
  </div>
))}

// ❌ 用 index 作 key(列表会增删排序时是 bug)
{list.map((item, index) => <Item key={index} data={item} />)}

// ✅ 稳定唯一 ID
{list.map((item) => <Item key={item.id} data={item} />)}
// 渲染数组的其它写法
// ① 先 map 成数组(推荐)
const items = list.map((x) => <li key={x.id}>{x.name}</li>);
return <ul>{items}</ul>;

// ② 用 Fragment 包多个兄弟元素
{list.map((item) => (
  <Fragment key={item.id}>
    <dt>{item.term}</dt>
    <dd>{item.desc}</dd>
  </Fragment>
))}

// ③ 简写形式不能带 key,需要 key 时必须用 <Fragment key>
{list.map((item) => (
  <React.Fragment key={item.id}>{item.name}</React.Fragment>
))}

key 的深入讨论(为什么不能用 index、原理是什么)见 4.5 key 的作用与选择。

五、属性(Props)的写法

// ① 字符串字面量用引号(不写引号则是表达式)
<div className="box" tabIndex={0} aria-label="提示" />

// ② 表达式用花括号
<img src={user.avatar} alt={user.name} width={size} />

// ③ 布尔属性
<input disabled />                    // 等价于 disabled={true}
<input disabled={false} />            // 不生效
<input required={isRequired} />

// ④ 展开传递(把对象的所有属性传下去)
const props = { id: 'a', className: 'x', onClick: handleClick };
<div {...props} />

// ⑤ 展开后覆盖(后面的生效)
<div {...props} className="override" />       // className 被覆盖为 override

// ⑥ 事件用驼峰、函数引用
<button onClick={handleClick} />              // ✅
<button onclick={handleClick} />              // ⚠️ 警告(原生写法,React 不识别)

// ⑦ 自定义属性用 data-* / aria-*
<div data-testid="box" aria-hidden="true" />

属性名的变化(HTML → JSX):

HTML JSX 原因
class className class 是 JS 保留字(历史上)
for htmlFor for 是 JS 保留字
tabindex tabIndex 驼峰命名约定
onclick onClick 同上
style="color: red" style={{ color: 'red' }} 接受对象而非字符串
contenteditable contentEditable 驼峰
aria-* / data-* 保持连字符写法 标准属性
colspan / rowspan colSpan / rowSpan 驼峰

六、样式与 className

// ① 内联 style 是对象(属性名用驼峰)
<div style={{ color: 'red', fontSize: 14, backgroundColor: '#fff' }} />

// ② className 拼接
<div className={`card ${isActive ? 'active' : ''}`} />
// 或用 clsx / classnames 库
<div className={clsx('card', { active: isActive }, size && `card--${size}`)} />

// ③ 条件样式
<div style={{ display: isVisible ? 'block' : 'none' }} />

// ④ CSS 变量
<div style={{ '--progress': `${percent}%` }} />
// ⚠️ 常见错误
<div style="color: red" />                    // ❌ 报错:style 需要对象
<div style={{ color: 'red', 'font-size': 14 }} />  // ❌ 应写成 fontSize
<div class="box" />                           // ⚠️ 警告:应写 className
<div style={{ fontSize: '14px' }} />          // ✅ 值需要带单位(除无单位属性)

七、语法约束

// ① 必须有一个根节点(或用 Fragment)
return (
  <div>
    <A />
    <B />
  </div>
);
// Fragment 简写:不会产生额外 DOM
return (
  <>
    <A />
    <B />
  </>
);

// ② 标签必须闭合
<div />                 // ✅
<input />               // ✅ 自闭合
<div></div>             // ✅
<br>                    // ❌ 必须写 <br />

// ③ 组件名必须大写开头(否则被当作 HTML 标签)
<MyComponent />         // ✅ 组件
<myComponent />         // ❌ 被当成自定义 HTML 元素,不会渲染组件
<my-component />        // ⚠️ 同上

// ④ 用小写点的形式引用组件(适合组件库导出)
import * as Form from './Form';
<Form.Input />          // ✅ 会被当作组件(有点号)

// ⑤ 注释要用花括号包起来
<div>
  {/* 这是 JSX 注释 */}
</div>

// ⑥ 属性值不能是未定义变量(会变成 undefined 属性)
<div title={undefined} />       // 属性不会输出(等于没写)

八、常见坑

// ① && 遇到 0 会渲染出 0(见上文)
{count && <List />}

// ② key 加错位置
{list.map((x) => <div><Item key={x.id} /></div>)}   // ⚠️ 应加在最外层

// ③ 用 index 作 key
{list.map((x, i) => <Item key={i} />)}              // ⚠️ 列表会变动时出 bug

// ④ 直接渲染对象
<div>{user}</div>                                    // ❌ 报错

// ⑤ 在 JSX 里写语句
{list.forEach((x) => console.log(x))}                // ⚠️ forEach 返回 undefined,不渲染
{list.map((x) => { <Item /> })}                      // ❌ 箭头函数用了 {} 但没 return

// ⑥ 箭头函数返回对象要加括号
{list.map((x) => ({ id: x.id }))}                    // ✅
{list.map((x) => { id: x.id })}                      // ❌ 被当作代码块

// ⑦ 拼接字符串时忘了空格
<div>{a}{b}</div>                                    // 'ab'(没有空格)
<div>{a} {b}</div>                                   // 'a b'

// ⑧ 布尔属性写成字符串
<button disabled="false">                            // ⚠️ 字符串 'false' 是「真值」
<button disabled={false}>                            // ✅

// ⑨ 中文与英文之间的空格需要在 JSX 里显式写
<p>请点击<a href="#">这里</a>查看</p>                 // ✅ 或显式写 {' '}

// ⑩ 上下文之外使用 JSX 需要正确配置(.js 里写 JSX 要改 .jsx 或配 babel)

面试延伸

  1. 「JSX 是什么?编译后变成什么?」
    JSX 是 React.createElement() 的语法糖(React 17+ 是 jsx / jsxs 运行时)。编译后返回一个 React Element 对象(含 type / props / key / ref / $$typeof),它是普通 JS 对象而不是 DOM 节点,也不是字符串。React 在后续渲染阶段才根据它创建/更新真实 DOM。

  2. 「为什么 React Element 上要加 $$typeof?」
    因为它是 Symbol 类型,JSON.parse 无法伪造 Symbol。早期 React 允许服务端返回 JSON 后直接渲染,攻击者可以构造一个 type 为恶意标签或携带危险 props 的「假 Element」注入页面。加上 Symbol 标识后,只渲染「由 React 真正创建」的元素,堵住了这条 XSS 途径。

  3. 「JSX 里为什么不能写 if / for?」
    因为 { } 里要求的是表达式(有返回值),而 if / for 是语句(不产生值)。这也正是 JSX 里大量使用三元表达式和逻辑与的原因。需要多分支逻辑时,通常把计算提到 JSX 外部(用变量或提前 return),或用映射表的方式。

  4. 「{count && <List />} 有什么问题?」
    当 count 为 0 时,0 && ... 的结果是 0,而 React 会把数字 0 渲染成文本(0 是「假值但可渲染」的值),于是页面上出现一个 0。同理 NaN 也有这个问题(会被渲染成 NaN)。修复方式是改用 count > 0 && ...、!!count && ... 或三元表达式 —— 即保证左侧是布尔值。

  5. 「哪些值会被 React 忽略、哪些会被渲染?」
    会被忽略(什么都不渲染):null、undefined、false、true、空数组。
    会被渲染:字符串、数字(包括 0 和 NaN)、元素、数组(逐项渲染)、Fragment。
    会报错:普通对象(Objects are not valid as a React child)、函数。
    这个差异正是「&& 的 0 陷阱」的根源。

  6. 「React 17 的自动运行时(Automatic Runtime)改了什么?」
    ① 不再需要 import React from 'react'——编译器自动从 react/jsx-runtime 注入 jsx / jsxs 函数(这也让未使用的 React 变量不再出现在产物中);② 生成的是 jsx() 调用而不是 React.createElement(),参数结构不同(children 放进 props);③ jsx 与 jsxs 分开处理静态与动态子节点,略微提升性能并改善 key 的检查。这是 React 17 引入、React 18/19 默认使用的模式。

  7. 「<Comp /> 和 Comp() 有什么区别?」
    <Comp /> 是创建元素并交给 React 渲染(React 会把它作为独立的 Fiber 节点处理,Hooks 归属正确、有自己的状态与生命周期);Comp() 是直接把函数当作普通函数调用,返回值被内联到父组件的渲染结果中——它的 Hooks 会被挂到父组件身上,导致状态混乱、调用顺序错乱。所以永远用 <Comp /> 而不是 Comp()。

  8. 「JSX 里的注释怎么写?为什么 { /* */ }?」
    因为 JSX 的 { } 里是 JS 表达式上下文,所以注释必须写成 {/* ... */} 的形式(外层花括号进入表达式上下文,里层是 JS 注释)。在 JSX 元素外部(如函数体里)可以直接写 // 或 /* */。

一句话速记

JSX 是 createElement 的语法糖,产物是不可变的 React Element 对象(用 Symbol 防伪造);{ } 里只能写表达式,所以要靠三元与 && 做条件渲染;{count && <X/>} 在 count === 0 时会渲染出 0(左侧要保证是布尔);列表用稳定唯一 key 并加在 map 返回的最外层元素上;样式是对象、类名是 className;React 17+ 不需要 import React,因为编译器会自动注入 jsx 运行时。

赞(0) 打赏
未经允许不得转载:陌上寒 » 1.1 JSX 语法与本质

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫