351 lines
12 KiB
Markdown
351 lines
12 KiB
Markdown
|
|
---
|
|||
|
|
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 的作用至关重要。
|
|||
|
|
|