REACT · Vol.I · LESSON 02 · 核心原语
JSX 详解:它到底是模板还是代码?
JSX 到底是不是 HTML?
上一篇文章里,这个 Counter 组件的 return 后面写了一段「很像 HTML」的东西。这段东西叫 JSX:
const el = <h1 className="title">Hello, {name}</h1>;
你第一反应可能是:「这不就是 HTML 吗?」——但不是。它写在 .tsx/.jsx 文件里,是 JavaScript 代码,不是字符串,也不是服务器端模板。
className 而不是 class 了吗?这是第一个线索:JSX 并不是原封不动的 HTML,它有自己的规则。为什么?因为它本质上是 JavaScript 表达式——长得像 HTML,只是为了让「界面长什么样」这件事一眼可读。Babel 把 JSX 编译成了什么?
浏览器根本不认识 JSX。写好的 .tsx 文件经过 Babel(或 esbuild/tsc)编译后,上面的 JSX 会变成普通的 JS 函数调用:
// 你写的 JSX
const el = <h1 className="title">Hello, {name}</h1>;
// 编译后(简化)
const el = React.createElement(
"h1", // 元素名
{ className: "title" }, // 属性对象
"Hello, ", name // 子节点
);
这揭示了一个重要事实:JSX 是 React.createElement 的语法糖。React 17 之后,编译器默认改写为自动导入的 jsx()(来自 react/jsx-runtime),你不再需要手动 import React,但本质完全一样——一段「描述界面结构」的 JavaScript 表达式。
这和你熟悉的 JSP / Thymeleaf 是两种完全不同的东西:
- JSP/Thymeleaf:在 HTML 里嵌入少量代码,本质还是模板字符串,运行时靠字符串拼接生成 HTML。
- JSX:整段都是代码,编译成对象(称为「React 元素」),最终由 React 转成真实 DOM。
用 Java 世界打个比方:JSP 是 String.format(),JSX 则是 Builder 模式构建出来的对象树——一个是拼字符串,一个是搭数据结构,后者才能被 diff、被复用、被类型检查。
必须掌握的 4 条规则
规则 1:用 {} 嵌入任意 JS 表达式
const user = { name: "wan", age: 26 };
<div>
{"名字: " + user.name} {/* 字符串拼接 */}
{"年龄: " + user.age + 1} {/* 结果是 "年龄: 261",字符串拼接 */}
{"年龄: " + (user.age + 1)} {/* 结果是 "年龄: 27",括号先算 */}
</div>
注意:{} 里只能放表达式,不能放语句。你可以放 user.name、三目运算、map(),但不能写 if、for(这些要挪到 JSX 外面或换成表达式)。
规则 2:返回时必须有一个「根」
// ❌ 错误:两个并列的根
return (
<h1>标题</h1>
<p>正文</p>
);
// ✅ 正确:包一层,或用空标签
return (
<>
<h1>标题</h1>
<p>正文</p>
</> {/* Fragment:编译后不产生任何 DOM 节点 */}
);
为什么非要一个根?回到编译真相:JSX 编译成的是 一个 函数调用的返回值,函数 return 只能返回「一个东西」。Fragment(<></>)就是为「想并列多个元素、又不想多加一层无意义 div」准备的标准解法。
规则 3:属性名用「驼峰」
| HTML 写法 | JSX 写法 | 原因 |
|---|---|---|
class | className | class 是 JS 保留字 |
for | htmlFor | for 也是 JS 保留字 |
onclick | onClick | React 事件系统,统一驼峰 |
tabindex | tabIndex | 遵循 DOM 属性驼峰规范 |
规则 4:注释要写在 {} 里
JSX 里直接 <!-- --> 是无效的,要用 {/* 注释 */}。因为它不是 HTML 注释的宿主,而是 JS 表达式——注释自然也要用 JS 的语法。
- JSX 是代码,不是字符串模板,编译成
createElement/jsx()调用。 {}里放任意 JS 表达式(不能放语句),用来「插值」。- 属性名驼峰、单个根(或 Fragment)、注释用
{/* */}。
条件渲染:把 if/else 搬进 JSX
规则 1 说「{} 里不能写 if」,那「根据条件显示不同内容」这个最常见的需求怎么办?答案是用表达式来表达条件。三板斧,覆盖所有场景:
板斧 1:三目运算符 —— 二选一
<div>
{isLoggedIn
? <button onClick={logout}>退出登录</button>
: <button onClick={login}>去登录</button>}
</div>
板斧 2:&& 短路 —— 有则显示,无则不显示
<div>
消息中心
{unreadCount > 0 && <span className="badge">{unreadCount}</span>}
</div>
&& 读作「前者成立时,才渲染后者」。对应 Java 里 if (x > 0) show(x) 的单分支。
板斧 3:提前 return —— 分支太复杂时跳出 JSX
function UserList({ loading, error, users }) {
if (loading) return <p>加载中…</p>;
if (error) return <p>出错了:{error.message}</p>;
if (users.length === 0) return <p>暂无用户</p>;
// 走到这里,只剩「正常渲染」一条路,JSX 干干净净
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
&&;「A 或 B」二选一看三目;超过两个分支就提前 return。JSX 里嵌套超过两层三目,可读性会断崖式下跌——这时候跳出 JSX 才是正解,这和 Java 里「用卫语句消灭深层嵌套」是同一种品味。0 && <X />&& 左侧是数字 0 时,React 会把 0 渲染出来(页面上凭空多个「0」),因为 0 在 React 里是合法的可渲染内容。写 {count > 0 && ...} 让左侧恒为布尔值,就能躲开。
动态属性、内联样式与 XSS 防线
属性值也能用 {}
<img src={user.avatar} alt={user.name} />
// 属性太多?像传参一样整体「展开」:
const props = { type: "text", placeholder: "请输入", maxLength: 20 };
<input {...props} /> // 等价于把三个属性逐个写上
{...props} 就是 JS 的对象展开语法,对应 Java 里把一整个 Map 传进工具方法,而不是一个个取值。
内联样式:注意,是「对象」不是字符串
// ❌ HTML 习惯:字符串。在 JSX 里直接报错
<div style="color: red; font-size: 14px">...</div>
// ✅ JSX:双层花括号 = 外层是「JS 表达式」,内层是「样式对象」
<div style={{ color: "red", fontSize: 14 }}>...</div>
// 样式随状态变化时尤其顺手:
<div style={{ color: done ? "#999" : "#1C1C1C" }}>{text}</div>
外层 {} 是 JSX 插值,内层 {} 是 JS 对象字面量——两层各干各的。属性名同样驼峰:font-size 写成 fontSize。日常开发里静态样式仍以 CSS 文件为主,内联样式主要用在「样式由 state 算出来」的场景。
JSX 天然防 XSS:自动转义
后端同学对 XSS(跨站脚本)再熟悉不过:用户输入直接拼进 HTML,就可能被注入 <script>。React 在这一点上是默认安全的——JSX 里插值的内容一律当纯文本处理,渲染前自动转义:
const evil = "<script>alert('xss')</script>";
<p>{evil}</p>
// 页面上显示字符串 "<script>alert('xss')</script>"
// 而不是真的插入一个 script 标签 ✅
对应到 Java 世界:这相当于模板引擎默认开了 th:text 式的 HTML 转义。唯一的例外是 dangerouslySetInnerHTML(等价于 th:utext),用来渲染富文本——名字起得这么吓人,就是提醒你:用它之前,必须先在服务端做过净化。
- 条件渲染三板斧:
&&显隐、三目二选一、提前 return 处理多分支。 - 属性可插值、可
{...spread}展开;内联style是对象,属性名驼峰。 - JSX 插值自动转义,默认免疫 XSS;
dangerouslySetInnerHTML是唯一例外,慎用。
新手报错清单:看见这些别慌
把最容易撞上的编译/运行时报错汇总成一张表,遇到的时候对号入座:
| 报错/现象 | 八成原因 | 修法 |
|---|---|---|
Adjacent JSX elements must be wrapped in an enclosing tag | return 里有多个并列根元素 | 包 Fragment <>...</> |
class is a reserved word / 样式不生效 | 写了 class= | 改成 className= |
style prop expects a mapping | style 传了字符串 | 改成对象 style={{ }} |
SyntaxError: Unexpected token, expected "," | 在 {} 里写了 if/for 语句 | 换成三目、&& 或提前 return |
页面凭空出现一个 0 | {count && <X/>} 且 count 为 0 | 改成 {count > 0 && ...} |
JSX 里的 <!-- --> 显示在页面上 | 用了 HTML 注释语法 | 改成 {/* */} |
本章回顾
JSX 的本质是「用 HTML 的壳,写 JavaScript 的骨」。理解这一点,你就不会再把 JSX 当成模板语言去死记它的「限制」,而是会用 JavaScript 的思维去驾驭它——因为 {} 里本来就是一个完整的 JS 表达式世界:能插值、能条件、能循环、能展开。
动手试一遍最能建立手感:把上一篇那个 Counter 组件里的 {count} 换成 {count % 2 === 0 ? "偶数" : "奇数"},再加一行 {count > 10 && <p>超过十次了!</p>}。两个都跑通,本篇的三板斧就是你的了。
Comments · 评论