8.5 KiB
8.5 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-04-29 22:01 |
JSX 语法
概述
JSX(JavaScript XML)是 React 的核心语法糖。它让开发者能够在 JavaScript 中直接编写类似 HTML 的结构,经编译后转化为 React.createElement() 调用。理解 JSX 的底层机制是掌握 React 渲染原理的基础。
JSX 的本质
JSX 并不是模板字符串,编译后会变成普通的 JavaScript 对象:
// 你写的 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 表达式:
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,常见陷阱
条件渲染模式
// 模式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
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 事件绑定
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>
);
}
事件传参
当需要向事件处理函数传递参数时,有两种写法:
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}(不要加括号)
常用合成事件类型
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 时有几条硬性规则需要遵守:
// 规则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 传递方式
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 与模板引擎对比
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 的核心优势
- 强类型 — 配合 TypeScript 编译期拦截错误
- 零额外语法 — 没有 {{}} / v-if / v-for 等新规则,一切在 JS 中
- JS 全能力 — 解构、spread、闭包、高阶函数随时可用
JSX 渲染流程
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 真实 DOMJSX 的每次重新渲染都经历完整的 Diff 过程——这也是为什么理解 key 的作用至关重要。