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

12 KiB
Raw Blame History

tags, create time
tags create time
React
JSX
TSX
Frontend
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 事件绑定

[!tip] 与 HTML 事件的核心区别

  • HTML: onclick="handler()" → 字符串属性,浏览器运行时解析,在全局作用域求值
  • JSX: onClick={handler} → 驼峰命名,接收一个 JS 函数引用作为 props,不是字符串
  • JSX 没有"字符串属性"的概念——所有 {} 内都是 JavaScript 表达式
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>
  );
}

事件传参

当需要向事件处理函数传递参数时,有两种写法:

interface TodoItemProps {
  id: number;
  onDelete: (id: number) => void;
}

function TodoItem({ id, onDelete }: TodoItemProps) {
  return (
    <button onClick={() => onDelete(id)}>
      {/* ⚡ 箭头函数包裹:创建闭包捕获当前 id,延迟到点击时执行 */}
      删除
    </button>
  );
}

[!question] "闭包捕获当前 id"到底什么意思?

类比:不用箭头函数就像点餐时一次性把所有订单全交给后厨(渲染时立刻执行);用箭头函数是给每个商品贴一张任务卡——卡片上记着当时的 id,等用户点击时才展开执行。

代码演示(以列表渲染为例):

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)}

常用合成事件类型

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 的核心优势

  1. 强类型 — 配合 TypeScript 编译期拦截错误
  2. 零额外语法 — 没有 {{}} / v-if / v-for 等新规则,一切在 JS 中
  3. 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 真实 DOM

JSX 的每次重新渲染都经历完整的 Diff 过程——这也是为什么理解 key 的作用至关重要。