
一句话:JSX 是
React.createElement()(React 17+ 是jsx()运行时)的语法糖,编译后返回一个普通的 JavaScript 对象(React Element);它既不是字符串也不是 HTML,因此有自己的一套约束。
一、JSX 是什么
// 你写的
const element = Hello, {name}!
;
// 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 = ;
// React 17+:不需要导入 React,编译器自动注入 jsx 运行时
const el = ;
| 维度 | 经典运行时 | 自动运行时(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];
// ✅ 可以写:任何「表达式」
// ❌ 不能写:语句
// 需要写逻辑时,把计算提到 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 ; return ; })()}
return (
{filtered.map((x) => )}
{total}
);
}
三种值的渲染规则:
| 值 | 渲染结果 |
|---|---|
| 字符串、数字 | 直接显示 |
null / undefined / false / true |
什么都不渲染(可用于条件渲染) |
| 数组 | 逐个渲染(每项需要 key) |
| 普通对象 | ❌ 报错(Objects are not valid as a React child) |
// ⚠️ 常见的「对象渲染报错」
const user = { name: '张三' };
return {user}; // ❌ 报错
return {user.name}; // ✅
return {JSON.stringify(user)}; // ✅(如果只是想展示)
三、条件渲染的写法
// ① 三元(最常用,适合二选一)
{isLogin ? : }
// ② 逻辑与(只关心「有或没有」)
{list.length > 0 &&
}
// ③ 提前 return(组件级条件渲染,可读性最好)
function Page({ loading, data }) {
if (loading) return ;
if (!data) return ;
return ;
}
// ④ 变量提取
let content;
if (loading) content = ;
else if (error) content = ;
else content = ;
return {content};
// ⑤ 映射表(多分支场景最清晰)
const views = {
loading: ,
error: ,
empty: ,
success: ,
};
return {views[status]};
⚠️ && 的 0 陷阱(最高频的 JSX 坑):
// ❌ count 为 0 时会渲染出「0」
{count &&
}
// 渲染结果:0
// 原因:0 是「假值但可渲染」的值 —— React 会把它当作子节点渲染出来
// (而 false / null / undefined / true 会被忽略)
// ✅ 四种修复
{count > 0 &&
}
{!!count &&
}
{Boolean(count) &&
}
{count ?
: null}
| 值 | {value && <X />} 的结果 |
|---|---|
0 |
渲染出 0 ⚠️ |
''(空字符串) |
渲染出空(不可见,但仍是文本节点) |
NaN |
渲染出 NaN ⚠️ |
null / undefined / false / true |
什么都不渲染 ✅ |
[](空数组) |
什么都不渲染 ✅ |
四、列表渲染与 key
// 基本写法
{list.map((item) => (
// ⭐ key 必须在 map 返回的最外层元素上
))}
// ❌ 常见错误:key 加在了内部元素上
{list.map((item) => (
{/* ⚠️ 警告 + 失效 */}
))}
// ❌ 用 index 作 key(列表会增删排序时是 bug)
{list.map((item, index) => )}
// ✅ 稳定唯一 ID
{list.map((item) => )}
// 渲染数组的其它写法
// ① 先 map 成数组(推荐)
const items = list.map((x) => {x.name} );
return {items}
;
// ② 用 Fragment 包多个兄弟元素
{list.map((item) => (
{item.term}
{item.desc}
))}
// ③ 简写形式不能带 key,需要 key 时必须用
{list.map((item) => (
{item.name}
))}
key的深入讨论(为什么不能用 index、原理是什么)见 [4.5 key 的作用与选择](../04-渲染机制与原理/4.5-key的作用与选择.md)。
五、属性(Props)的写法
// ① 字符串字面量用引号(不写引号则是表达式)
// ② 表达式用花括号
// ③ 布尔属性
// 等价于 disabled={true}
// 不生效
// ④ 展开传递(把对象的所有属性传下去)
const props = { id: 'a', className: 'x', onClick: handleClick };
// ⑤ 展开后覆盖(后面的生效)
// className 被覆盖为 override
// ⑥ 事件用驼峰、函数引用
// ✅
// ⚠️ 警告(原生写法,React 不识别)
// ⑦ 自定义属性用 data-* / aria-*
属性名的变化(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 是对象(属性名用驼峰)
// ② className 拼接
// 或用 clsx / classnames 库
// ③ 条件样式
// ④ CSS 变量
// ⚠️ 常见错误
// ❌ 报错:style 需要对象
// ❌ 应写成 fontSize
// ⚠️ 警告:应写 className
// ✅ 值需要带单位(除无单位属性)
七、语法约束
// ① 必须有一个根节点(或用 Fragment)
return (
);
// Fragment 简写:不会产生额外 DOM
return (
<>
>
);
// ② 标签必须闭合
// ✅
// ✅ 自闭合
// ✅
// ❌ 必须写
// ③ 组件名必须大写开头(否则被当作 HTML 标签)
// ✅ 组件
// ❌ 被当成自定义 HTML 元素,不会渲染组件
// ⚠️ 同上
// ④ 用小写点的形式引用组件(适合组件库导出)
import * as Form from './Form';
// ✅ 会被当作组件(有点号)
// ⑤ 注释要用花括号包起来
{/* 这是 JSX 注释 */}
// ⑥ 属性值不能是未定义变量(会变成 undefined 属性)
// 属性不会输出(等于没写)
八、常见坑
// ① && 遇到 0 会渲染出 0(见上文)
{count &&
}
// ② key 加错位置
{list.map((x) => )} // ⚠️ 应加在最外层
// ③ 用 index 作 key
{list.map((x, i) => )} // ⚠️ 列表会变动时出 bug
// ④ 直接渲染对象
{user} // ❌ 报错
// ⑤ 在 JSX 里写语句
{list.forEach((x) => console.log(x))} // ⚠️ forEach 返回 undefined,不渲染
{list.map((x) => { })} // ❌ 箭头函数用了 {} 但没 return
// ⑥ 箭头函数返回对象要加括号
{list.map((x) => ({ id: x.id }))} // ✅
{list.map((x) => { id: x.id })} // ❌ 被当作代码块
// ⑦ 拼接字符串时忘了空格
{a}{b} // 'ab'(没有空格)
{a} {b} // 'a b'
// ⑧ 布尔属性写成字符串
面试延伸
- 「JSX 是什么?编译后变成什么?」
JSX 是 React.createElement() 的语法糖(React 17+ 是 jsx / jsxs 运行时)。编译后返回一个 React Element 对象(含 type / props / key / ref / $$typeof),它是普通 JS 对象而不是 DOM 节点,也不是字符串。React 在后续渲染阶段才根据它创建/更新真实 DOM。
- 「为什么 React Element 上要加
$$typeof?」
因为它是 Symbol 类型,JSON.parse 无法伪造 Symbol。早期 React 允许服务端返回 JSON 后直接渲染,攻击者可以构造一个 type 为恶意标签或携带危险 props 的「假 Element」注入页面。加上 Symbol 标识后,只渲染「由 React 真正创建」的元素,堵住了这条 XSS 途径。
- 「JSX 里为什么不能写
if/for?」
因为 { } 里要求的是表达式(有返回值),而 if / for 是语句(不产生值)。这也正是 JSX 里大量使用三元表达式和逻辑与的原因。需要多分支逻辑时,通常把计算提到 JSX 外部(用变量或提前 return),或用映射表的方式。
- 「
{count && <List />}有什么问题?」
当 count 为 0 时,0 && ... 的结果是 0,而 React 会把数字 0 渲染成文本(0 是「假值但可渲染」的值),于是页面上出现一个 0。同理 NaN 也有这个问题(会被渲染成 NaN)。修复方式是改用 count > 0 && ...、!!count && ... 或三元表达式 —— 即保证左侧是布尔值。
- 「哪些值会被 React 忽略、哪些会被渲染?」
会被忽略(什么都不渲染):null、undefined、false、true、空数组。 会被渲染:字符串、数字(包括 0 和 NaN)、元素、数组(逐项渲染)、Fragment。 会报错:普通对象(Objects are not valid as a React child)、函数。 这个差异正是「&& 的 0 陷阱」的根源。
- 「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 默认使用的模式。
- 「
<Comp />和Comp()有什么区别?」
<Comp /> 是创建元素并交给 React 渲染(React 会把它作为独立的 Fiber 节点处理,Hooks 归属正确、有自己的状态与生命周期);Comp() 是直接把函数当作普通函数调用,返回值被内联到父组件的渲染结果中——它的 Hooks 会被挂到父组件身上,导致状态混乱、调用顺序错乱。所以永远用 <Comp /> 而不是 Comp()。
- 「JSX 里的注释怎么写?为什么
{ /* */ }?」
因为 JSX 的 { } 里是 JS 表达式上下文,所以注释必须写成 {/* ... */} 的形式(外层花括号进入表达式上下文,里层是 JS 注释)。在 JSX 元素外部(如函数体里)可以直接写 // 或 /* */。
一句话速记
JSX 是
createElement的语法糖,产物是不可变的 React Element 对象(用Symbol防伪造);{ }里只能写表达式,所以要靠三元与&&做条件渲染;{count && <X/>}在count === 0时会渲染出0(左侧要保证是布尔);列表用稳定唯一key并加在map返回的最外层元素上;样式是对象、类名是className;React 17+ 不需要import React,因为编译器会自动注入jsx运行时。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!