This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/REACT/1. 基础篇/02-JSX 语法.md
T
2026-04-29 22:19:51 +08:00

297 lines
8.5 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 事件绑定
```jsx
function ClickCounter() {
const [count, setCount] = useState(0);
// 驼峰命名,不要写 onClick="handler()"
const handleClick = () => setCount(c => c + 1);
// 阻止默认行为
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>
);
}
```
> [!warning] 常见陷阱
> - ~~`onClick={onDelete(id)}`~~ ❌ — 会在渲染时立即执行 `onDelete`,而非点击时执行
> - ~~`onClick={onDelete}`~~ — 缺少参数,不知道要删除哪个
> - ✅ **正确姿势**:`() => onDelete(id)` — 用箭头函数延迟执行并捕获参数
> - 💡 如果不需要参数,直接传引用即可:`onClick={handleClick}`(不要加括号)
### 常用合成事件类型
```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 的作用至关重要。