
一句话: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)
面试延伸
- 「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。学习了。。。。
网站不错!!!!
简洁实用,好文章!