Files
cs-note/hhs/REACT/1. 基础篇/02-JSX 语法.md
T
2026-05-24 11:42:38 +08:00

351 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [React, JSX, TSX, Frontend]
create time: 2026-04-29 22:01
---
# JSX 语法
## 概述
JSX(JavaScript XML)是 React 的核心语法糖。它让开发者能够在 JavaScript 中直接编写类似 HTML 的结构,经编译后转化为 `React.createElement()` 调用。理解 JSX 的底层机制是掌握 React 渲染原理的基础。
## JSX 的本质
JSX 并不是模板字符串,编译后会变成普通的 JavaScript 对象:
```jsx
// 你写的 JSX
const element = <h1 className="title">Hello, World!</h1>;
// 编译后的产物
const element = React.createElement(
"h1",
{ className: "title" },
"Hello, World!"
);
```
> [!warning] 关键点
> - `React.createElement` 返回的是一个 **Plain Object**(虚拟 DOM 节点),不是真实的 DOM 元素
> - 这也是为什么 JSX 不会直接操作浏览器 API,而是由 React 的 Reconciler 统一管理
## 表达式嵌入
在 JSX 中用 `{}` 包裹 JavaScript 表达式:
```jsx
function FormattedDate({ date }) {
return (
<>
<h1>Hello!</h1>
<h2>今天 {date.toLocaleDateString()}.</h2>
{/* 条件:JSX 不能用 if-else,用三元运算符或逻辑与 */}
{date.getTime() < Date.now() && <p>日期已过期</p>}
{/* 数组渲染:每个元素需要 key prop */}
{[1, 2, 3].map(n => (
<span key={n}>{n}</span>
))}
</>
);
}
```
### 允许与不允许的表达式
| ✅ 允许 | ❌ 不允许 |
|---------|----------|
| `{variable}` | `{if (x) ...}` — JSX 中不支持语句 |
| `{arr.map(...)}` | `{for (let i...)}` — 同上 |
| `{cond && <Comp />}` | `{func()}` — 函数无返回值时渲染 `undefined`(不报错但不生效)|
| `{null}`, `{false}` | `{""}`, `{0}` — 会渲染空字符串或数字 `0`,常见陷阱 |
> [!question] 为什么 `{cond ? <A /> : func()}` 能跑?
> - 这是合法的 JS 表达式——只要 `func()` 返回一个值即可
> - 但建议写成 `{cond ? <A /> : <B />}`,保持视觉一致性,避免混用 JSX 和函数调用
> [!note] null / undefined / boolean vs "" / 0
> - `null`、`undefined`、`true`、`false` 渲染结果为空——这是有意设计
> - `0` 和 `""` 会输出到 DOM,常见陷阱
## 条件渲染模式
```jsx
// 模式1:&& 短路(推荐用于简单场景)
{isLoggedIn && <AdminDashboard />}
// 模式2:三元运算符(适合两种视图切换)
{isLoading ? <Spinner /> : <Content />}
// 模式3:提取为组件(复杂条件逻辑)
return <UserCard user={user} showActions={isAdmin} />;
// 模式4:IIFE(极少使用,仅临时调试)
{(function () {
if (!data) return <EmptyState />;
return <DataDisplay data={data} />;
})()}
```
## 列表渲染与 Key
```tsx
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
function TodoList({ items }: { items: TodoItem[] }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
<input type="checkbox" checked={item.completed} readOnly />
{item.text}
</li>
))}
</ul>
);
}
```
> [!question] 思考:为什么 React 要求列表项必须有 key?
> - Key 帮助 React 识别哪些条目变了/增了/删了,从而最小化重渲染次数
> - 不建议用 index 作为 key(当列表顺序变化时会导致状态错乱)
## JSX 事件绑定
> [!tip] 与 HTML 事件的核心区别
> - **HTML**: `onclick="handler()"` → 字符串属性,浏览器运行时解析,在全局作用域求值
> - **JSX**: `onClick={handler}` → 驼峰命名,接收一个 **JS 函数引用**作为 props,不是字符串
> - JSX 没有"字符串属性"的概念——所有 `{} ` 内都是 JavaScript 表达式
```jsx
function ClickCounter() {
const [count, setCount] = useState(0);
// ✅ 正确:驼峰命名 onClick,值为函数引用(不加括号)
const handleClick = () => setCount(c => c + 1);
// ❌ 错误示范
// <button onClick="handleClick"> → 字符串,React 无法调用
// <button onClick={handleClick()}> → 渲染时立即执行,onClick = undefined
// <button onclick={handleClick}> → 小写 onclick 不会被 React 识别为事件
// 阻止默认行为
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
};
return (
<div>
<button onClick={handleClick}>Count: {count}</button>
<form onSubmit={handleSubmit}>
<input type="text" name="q" />
</form>
</div>
);
}
```
### 事件传参
当需要向事件处理函数传递参数时,有两种写法:
```tsx
interface TodoItemProps {
id: number;
onDelete: (id: number) => void;
}
function TodoItem({ id, onDelete }: TodoItemProps) {
return (
<button onClick={() => onDelete(id)}>
{/* ⚡ 箭头函数包裹:创建闭包捕获当前 id,延迟到点击时执行 */}
删除
</button>
);
}
```
> [!question] "闭包捕获当前 id"到底什么意思?
>
> **类比**:不用箭头函数就像点餐时一次性把所有订单全交给后厨(渲染时立刻执行);用箭头函数是给每个商品贴一张任务卡——卡片上记着当时的 `id`,等用户点击时才展开执行。
>
> **代码演示**(以列表渲染为例):
>
> ```tsx
> const todos = [{ id: 1, text: "买菜" }, { id: 2, text: "取快递" }];
>
> // ❌ onClick={onDelete(todo.id)} — 括号让它变成"调用语句"
> todos.map(todo => (
> // 第一次循环 → 立刻执行 onDelete(1),item1 被删 ✗
> // 第二次循环 → 立刻执行 onDelete(2),item2 被删 ✗
> // 页面还没展示,三个都删光了
> <button key={todo.id} onClick={onDelete(todo.id)}>删除</button>
> ));
>
> // ✅ onClick={() => onDelete(todo.id)} — 没有括号,只是"存一张任务卡"
> todos.map(todo => (
> // 每次循环创建新的箭头函数,每个闭包封存了当前的 todo.id
> // #1 的闭包记忆: todo.id = 1
> // #2 的闭包记忆: todo.id = 2
> // 用户点击哪个按钮,就取出对应的任务卡执行
> <button key={todo.id} onClick={() => onDelete(todo.id)}>
> 删除 {todo.text}
> </button>
> ));
>
> // ✅ 如果不需要参数,直接传引用即可
> <button onClick={handleClick}>点我</button>
> // 等价于 → <button onClick={() => handleClick()}>点我</button>
> // 但前者更简洁(没有额外创建箭头函数的开销)
> ```
>
> **什么是闭包?** 函数 + 它被创建时所在的词法环境。每次循环创建的箭头函数都是一个独立的闭包,记住了自己出生时的 `id` 值。即使后面 `id` 变了(比如在 for 循环中),闭包里的值也不会跟着变。
### 为什么需要包装?核心逻辑
| 情况 | 做法 | 原因 |
|------|------|------|
| 不需要传参 | `onClick={handler}` | 直接传函数引用,React 在点击时调用 |
| 需要传外部变量 | `onClick={() => handler(val)}` | 箭头函数作为中间层,传入所需参数 |
| 需要访问原事件对象 | `onClick={(e) => { e.stopPropagation(); handler(id); }}` | 同时处理事件和自定义参数 |
> [!warning] 常见陷阱
> - ~~`onClick="handler()"`~~ ❌ — 字符串赋值给 onClick prop,React 试图调用字符串时会报错
> - ~~`onClick={handler()}`~~ ❌ — 渲染时立即执行 handler,onClick = undefined(通常是函数返回值)
> - ~~`onClick={handler}`~~ ❌ — 缺少参数,不知道要操作哪个元素
> - ✅ **正确姿势**:无参直接传引用 `onClick={handleClick}`;有参用箭头函数延迟执行 `onClick={() => handler(id)}`
### 常用合成事件类型
```ts
type MouseEvents = React.MouseEvent<HTMLButtonElement>;
type FormEvents = React.FormEvent<HTMLFormElement>;
type ChangeEvents = React.ChangeEvent<HTMLInputElement>;
type KeyboardEvents = React.KeyboardEvent<HTMLDivElement>;
```
> [!tip] TypeScript 事件类型的推导技巧
> - 不传泛型时:`event.target` 类型为 `EventTarget`(信息最少)
> - 传入具体元素后:`event.currentTarget` 可以精确定位到对应 DOM 类型
> - 💡 **最佳实践**:优先使用 `currentTarget` 而非 `target`,避免类型收窄问题
## JSX 基本规则
写 JSX 时有几条硬性规则需要遵守:
```tsx
// 规则1:自闭合标签 —— 无子元素的元素必须加 /
<img src={avatar} alt="头像" />
<Input placeholder="搜索..." />
// 规则2:className 替代 class
<div className="card">内容</div>
// <div class="card"> ❌ — "class"是JS保留字
// 规则3:style 接收对象而非字符串
<div style={{ color: "red", fontSize: 16 }}>样式</div>
// 驼峰命名:fontSize, backgroundColor, gridColumn 等
// 规则4:单根节点或 Fragment
const Card = ({ title, children }) => (
<> {/* ✅ Fragment:不产生额外 DOM 节点 */}
<h2>{title}</h2>
<div>{children}</div>
</>
);
```
> [!note] key 属性的唯一要求
> - `key` 只在数组上下文中有效——它是 React 的元数据,不会被传递给组件
> - 在自定义组件上使用 key 会报错:**key 只能在列表子元素上使用**
## Props 传递方式
```tsx
interface CardProps {
title: string;
className?: string;
children: React.ReactNode;
}
function Card({ title, className = "", children }: CardProps) {
return (
<div className={`card ${className}`}>
<h3>{title}</h3>
{children}
</div>
);
}
// 展开 props
const attrs = { className: "card", title: "详情" };
<Card {...attrs} />;
// 动态属性名
<div style={{ color: isActive ? "red" : "black" }}>
// 受控 props(父组件控制子组件内部 state)
<Select value={selectedValue} onChange={setSelectedValue} />
// 函数作为 prop(常见于表单回调)
<SearchBar onSearch={(q) => setQuery(q)} />
```
> [!question] 为什么 children 的类型是 React.ReactNode 而不是 JSX.Element?
> - `JSX.Element` 只代表 React 元素(如 `<div />`、`<Comp />`)
> - `React.ReactNode` 包含更多可能:字符串、数字、Fragment、甚至 null/undefined
> - 💡 使用 `JSX.Element` 会报错当子元素是纯文本或数组时
## JSX 与模板引擎对比
```mermaid
graph LR
A[JSX] --> B["编译期检查"]
A --> C["完整的 JavaScript 能力"]
A --> D["类型安全(TS)"]
E["模板引擎 v-html/v-if"] --> F["运行时解析"]
E --> G["字符串插值 {{}}"]
E --> H["有限的 JS 表达式"]
style A fill:#61DAFB,color:#000
style E fill:#42B883,color:#fff
```
> [!tip] JSX 的核心优势
> 1. **强类型** — 配合 TypeScript 编译期拦截错误
> 2. **零额外语法** — 没有 {{}} / v-if / v-for 等新规则,一切在 JS 中
> 3. **JS 全能力** — 解构、spread、闭包、高阶函数随时可用
## JSX 渲染流程
```mermaid
sequenceDiagram
participant Dev as 开发者
participant Babel as Babel/TSX
participant React as React Runtime
participant VDOM as Fiber (虚拟 DOM)
participant Diff as Reconciler Diff
participant BM as Browser API
Dev->>Babel: 编写 JSX:<h1>Hello</h1>
Note over Dev,Babel: "编译期转换,非运行时"
Babel->>Babel: 转译成 createElement()
Babel->>React: React.createElement("h1", null, "Hello")
Note over React: 返回 VDOM Plain Object
React->>VDOM: 创建/更新 Fiber 树
VDOM->>Diff: Diff 算法比较新旧树
Diff->>Diff: 找出最小变更集
Diff->>BM: patch 真实 DOM
Note over BM: "批量更新,一次 Reflow/Repaint"
```
> [!note] 一句话总结渲染流程
> JSX → `createElement()` → VDOM → **Diff** → patch 真实 DOM
>
> JSX 的每次重新渲染都经历完整的 Diff 过程——这也是为什么理解 key 的作用至关重要。