首页 / React 学习笔记 / 02

REACT · Vol.I · LESSON 02 · 核心原语

JSX 详解:它到底是模板还是代码?

入门核心#核心#语法

JSX 到底是不是 HTML?

上一篇文章里,这个 Counter 组件的 return 后面写了一段「很像 HTML」的东西。这段东西叫 JSX

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(),但不能写 iffor(这些要挪到 JSX 外面或换成表达式)。

规则 2:返回时必须有一个「根」

错误 vs 正确
// ❌ 错误:两个并列的根
return (
  <h1>标题</h1>
  <p>正文</p>
);

// ✅ 正确:包一层,或用空标签
return (
  <>
    <h1>标题</h1>
    <p>正文</p>
  </>   {/* Fragment:编译后不产生任何 DOM 节点 */}
);

为什么非要一个根?回到编译真相:JSX 编译成的是 一个 函数调用的返回值,函数 return 只能返回「一个东西」。Fragment(<></>)就是为「想并列多个元素、又不想多加一层无意义 div」准备的标准解法。

规则 3:属性名用「驼峰」

HTML 写法JSX 写法原因
classclassNameclass 是 JS 保留字
forhtmlForfor 也是 JS 保留字
onclickonClickReact 事件系统,统一驼峰
tabindextabIndex遵循 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 传进工具方法,而不是一个个取值。

内联样式:注意,是「对象」不是字符串

style 的正确写法
// ❌ 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 tagreturn 里有多个并列根元素包 Fragment <>...</>
class is a reserved word / 样式不生效写了 class=改成 className=
style prop expects a mappingstyle 传了字符串改成对象 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 · 评论