React(二):JSX 语法与严格模式
目录
第一部分:JSX 语法
1.1 JSX 是什么?—— 一个生活化的类比
类比:建筑师画图纸
想象你是一位建筑师,你需要向施工队描述一栋房子的结构。
- 传统方式(原生 JS 操作 DOM):你得像写施工手册一样,一步一步告诉工人:"先在这里放一块砖,再在砖上面抹水泥,然后在水泥上面再放一块砖……"每一步都要精确到毫米,代码写起来非常繁琐。
// 原生 JS 创建一个带文字和样式的按钮——你得一步一步"指挥"浏览器
const btn = document.createElement("button"); // 第1步:造一个按钮
btn.className = "my-btn"; // 第2步:给它加个类名
btn.textContent = "点击我"; // 第3步:设置按钮上的文字
btn.addEventListener("click", () => {
// 第4步:绑定点击事件
alert("你好!");
});
const container = document.getElementById("app"); // 第5步:找到要挂载的位置
container.appendChild(btn); // 第6步:把按钮放进去
- JSX 方式:你直接在图纸上画出房子的样子——"这里是一扇门,这里是一扇窗,客厅在这里"。图纸直观地描述了最终结果,至于怎么施工,由施工队(React)自己搞定。
// JSX 写法——像写 HTML 一样描述界面,React 帮你搞定 DOM 操作
function App() {
return (
<button className="my-btn" onClick={() => alert("你好!")}>
点击我
</button>
);
}
一句话总结:JSX 是 JavaScript 的"语法糖外衣",让你用写 HTML 的方式去写 JavaScript 逻辑。它既不是 HTML,也不是字符串,而是 JavaScript 的语法扩展。
为什么要用 JSX?
| 对比维度 | 原生 JS 操作 DOM | JSX |
|---|---|---|
| 代码量 | 冗长,创建+属性+事件+挂载四步走 | 简洁,所见即所得 |
| 可读性 | 需要脑补最终效果 | 结构一目了然 |
| 维护性 | 增删元素要改多处 | 直接修改模板即可 |
| 学习成本 | 需要记忆大量 DOM API | 会 HTML 就能上手 |
1.2 JSX 基础语法规则
学习 JSX 就像学开车——有几个"交通规则"必须遵守,否则代码会直接报错。下面逐一拆解。
规则一:必须有且仅有一个根节点
JSX 表达式必须被一个父元素包裹。你可以把这个规则理解为:一棵树只能有一个树干。
// ❌ 错误写法:两个元素并列,JSX 不知道谁才是"老大"
function App() {
return (
<h1>标题</h1>
<p>段落内容</p>
);
}
// 浏览器报错:Adjacent JSX elements must be wrapped in an enclosing tag.
// ✅ 正确写法一:用 <div> 包裹
function App() {
return (
<div>
<h1>标题</h1>
<p>段落内容</p>
</div>
);
}
// ✅ 正确写法二:用 Fragment 空标签 <>...</> 包裹(推荐!)
// Fragment 就像一个"隐形盒子"——它把子元素装在一起,但不会在页面上多渲染一层 DOM
function App() {
return (
<>
<h1>标题</h1>
<p>段落内容</p>
</>
);
}
什么时候用
<div>,什么时候用<>...</>?
- 如果不需要额外的 DOM 节点,用
<>...</>(比如避免破坏 CSS 布局)- 如果需要给容器加样式或属性,用
<div>(Fragment 不能加 className 等属性)
规则二:所有标签必须闭合
在 HTML 中,<br> 和 <img> 可以不写闭合标签。但在 JSX 中,所有标签都必须闭合——就像出门必须穿鞋一样,没有例外。
// ❌ 错误写法:HTML 中的自闭合标签在 JSX 中不闭合会报错
function App() {
return (
<div>
<img src="logo.png"> {/* 缺少 /> */}
<br> {/* 缺少 /> */}
<input type="text"> {/* 缺少 /> */}
</div>
);
}
// ✅ 正确写法:自闭合标签必须以 /> 结尾
function App() {
return (
<div>
<img src="logo.png" alt="网站Logo" /> {/* 必须闭合 */}
<br /> {/* 必须闭合 */}
<input type="text" /> {/* 必须闭合 */}
</div>
);
}
规则三:class 必须写成 className
因为 class 是 JavaScript 的保留关键字(用于定义类),JSX 中表示 CSS 类名必须使用 className。
趣味记忆法:把
className理解为 "class 的名字",就像 "userName" 是 "user 的名字" 一样。
// ❌ 错误写法
function App() {
return <div class="container">内容</div>;
}
// 浏览器控制台警告:Invalid DOM property `class`. Did you mean `className`?
// ✅ 正确写法
function App() {
return <div className="container">内容</div>;
}
规则四:for 必须写成 htmlFor
for 同样是 JavaScript 的关键字(用于循环),所以 <label> 的 for 属性必须写成 htmlFor。
// ❌ 错误写法
function App() {
return (
<div>
<label for="username">用户名:</label>
<input id="username" type="text" />
</div>
);
}
// ✅ 正确写法
function App() {
return (
<div>
<label htmlFor="username">用户名:</label>
<input id="username" type="text" />
</div>
);
}
规则五:style 属性用对象写法
在 JSX 中,内联样式不能写字符串,必须写成 JavaScript 对象。同时,CSS 属性名要改为驼峰命名(camelCase)。
// ❌ 错误写法
function App() {
return <div style="color: red; font-size: 20px;">红色大字</div>;
}
// ❌ 还是错误:属性名不是驼峰
function App() {
return <div style={{ color: "red", "font-size": "20px" }}>红色大字</div>;
}
// ✅ 正确写法:外层的 {} 是 JSX 表达式语法,内层的 {} 是 JS 对象字面量
function App() {
return <div style={{ color: "red", fontSize: "20px" }}>红色大字</div>;
}
记忆技巧:
style={{ }}—— 两个花括号,里面那对是对象,外面那对是"我要写 JS 了"的信号。
规则六:事件命名用驼峰
JSX 中的事件属性使用驼峰命名,而且事件处理函数必须用 {} 包裹。
// ❌ 错误写法:HTML 风格的事件名
function App() {
return <button onclick="handleClick()">点击</button>;
}
// ✅ 正确写法:驼峰命名 + {} 包裹
function App() {
const handleClick = () => {
alert("按钮被点击了!");
};
return <button onClick={handleClick}>点击</button>;
}
基础规则速查表
| HTML 写法 | JSX 正确写法 | 原因 |
|---|---|---|
class="box" |
className="box" |
class 是 JS 关键字 |
for="id" |
htmlFor="id" |
for 是 JS 关键字 |
style="color:red" |
style={{ color: 'red' }} |
style 需要 JS 对象 |
onclick="fn()" |
onClick={fn} |
驼峰命名 + JS 表达式 |
tabindex="0" |
tabIndex={0} |
驼峰命名 |
<br> |
<br /> |
必须闭合 |
1.3 JSX 中嵌入 JavaScript 表达式
JSX 最强大的能力就是——它可以让你在"看起来像 HTML"的代码中随时切回 JavaScript 模式。
类比:翻译官的双语切换
想象你是一个中英双语翻译官。大部分时间你说中文(HTML 结构),但遇到专业术语时,你会秒切英文(JavaScript 表达式),说完再切回来。JSX 中的 {} 就是那个"切换信号"——遇到 {},React 就知道:"里面是 JavaScript,请用 JS 引擎执行它。"
表达式 vs 语句 —— 关键区别
这是初学者最容易混淆的概念。记住一条黄金法则:
{}中只能放"有结果"的东西(表达式),不能放"做事情"的东西(语句)。
| 类型 | 说明 | 举例 | 能不能放 {} 里? |
|---|---|---|---|
| 表达式 | 执行后会产生一个值 | 1 + 2、user.name、arr.map()、condition ? a : b |
✅ 能 |
| 语句 | 执行一个动作,不产生值 | if、for、while、switch、const x = 1 |
❌ 不能 |
生活化理解:表达式像是"问你一个问题,你给一个答案"("1+1等于几?"→ "2");语句像是"命令你做一件事"("去把垃圾倒了"→ 执行了动作,但没有"答案")。
示例一:插入变量
function Greeting() {
const name = "小明"; // 定义一个变量
const age = 18; // 定义另一个变量
return (
<div>
{/* 用 {} 把变量插入到 HTML 结构中 */}
<h1>你好,我叫 {name}</h1>
<p>我今年 {age} 岁</p>
{/* 花括号里可以做运算 */}
<p>明年我就 {age + 1} 岁了</p>
</div>
);
}
// 页面渲染结果:
// 你好,我叫 小明
// 我今年 18 岁
// 明年我就 19 岁了
示例二:调用函数
function App() {
// 定义一个格式化函数
const formatPrice = (price) => {
return "¥" + price.toFixed(2); // 保留两位小数,前面加 ¥
};
const productName = "机械键盘";
const price = 299.9;
return (
<div>
<h2>{productName}</h2>
{/* {} 里可以直接调用函数,用它的返回值 */}
<p>价格:{formatPrice(price)}</p>
{/* 也可以调用 JS 内置方法 */}
<p>{productName.toUpperCase()}</p> {/* 渲染:机械键盘 → 变成全大写 */}
</div>
);
}
// 页面渲染结果:
// 机械键盘
// 价格:¥299.90
// 机械键盘(这个会是全大写的效果:“Jī Xiè Jiàn Pán”?不对,toUpperCase 对中文无效,我们留下这个作为教学注意点)
示例三:三元运算
三元运算是 JSX 中最常用的"微决策"工具,因为 if 是语句,不能放在 {} 里面。
function UserStatus() {
const isLoggedIn = true; // 模拟登录状态
return (
<div>
{/* 三元表达式:条件 ? 为真时显示 : 为假时显示 */}
<p>当前状态:{isLoggedIn ? "已登录" : "未登录"}</p>
{/* 三元表达式可以嵌套,但不推荐太深 */}
<p>{isLoggedIn ? "欢迎回来!" : "请先登录"}</p>
</div>
);
}
// 渲染结果:
// 当前状态:已登录
// 欢迎回来!
示例四:数组渲染
function FruitList() {
const fruits = ["苹果", "香蕉", "橘子", "葡萄"];
return (
<div>
<h2>水果清单</h2>
<ul>
{/* map() 遍历数组,返回一个新的 JSX 元素数组 */}
{/* 注意:这里 map 返回的是一个数组,JSX 可以直接渲染数组 */}
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
</div>
);
}
// 渲染结果:
// 水果清单
// · 苹果
// · 香蕉
// · 橘子
// · 葡萄
常见错误写法示例
// ❌ 错误1:在 {} 中写 if 语句
function Wrong1() {
const isAdmin = true;
return (
<div>
{if (isAdmin) { <p>管理员面板</p> }} {/* 语法错误!if 是语句不是表达式 */}
</div>
);
}
// ❌ 错误2:在 {} 中写 for 循环
function Wrong2() {
return (
<ul>
{for (let i = 0; i < 5; i++) { <li>{i}</li> }} {/* 语法错误! */}
</ul>
);
}
// ❌ 错误3:在 {} 中写变量声明
function Wrong3() {
return (
<div>
{const name = '小明'} {/* 语法错误!const 声明是语句 */}
</div>
);
}
// ✅ 正确做法:把这些逻辑移到 return 之前
function Correct() {
// 在 return 之前处理逻辑
const isAdmin = true;
let adminPanel = null;
if (isAdmin) {
adminPanel = <p>管理员面板</p>;
}
return (
<div>
{adminPanel} {/* 这里只放表达式 */}
</div>
);
}
1.4 条件渲染
条件渲染的本质是:根据不同的数据状态,显示不同的 UI。
类比:交通信号灯
条件渲染就像路口红绿灯——状态不同,显示就不同(红灯→停,绿灯→行,黄灯→等一等)。React 也是根据"数据状态"来决定显示哪块 UI。
方式一:if-else(在 return 之前判断)
适用于二选一的复杂场景。
function WelcomeMessage({ user }) {
// 在 return 之前用 if-else 决定渲染内容
if (!user) {
// 没有用户数据:显示加载中
return (
<div className="loading">
<p>加载中,请稍候...</p>
</div>
);
}
if (user.role === "admin") {
// 管理员:显示特权界面
return (
<div className="admin-panel">
<h2>欢迎回来,管理员 {user.name}</h2>
<button>进入后台</button>
</div>
);
}
// 普通用户:显示常规界面
return (
<div className="user-panel">
<h2>你好,{user.name}</h2>
<button>查看个人信息</button>
</div>
);
}
方式二:&& 逻辑与(最简单常见)
适用于**"有就显示,没有就不显示"**的单条件场景。
function NotificationBell({ unreadCount }) {
return (
<div className="notification">
{/* 铃铛图标 */}
<span>🔔</span>
{/* && 短路逻辑:只有当 unreadCount > 0 时,红点才渲染 */}
{unreadCount > 0 && <span className="badge">{unreadCount}</span>}
{/* 如果没有未读消息,红点完全不存在于 DOM 中 */}
</div>
);
}
// unreadCount = 3 → 渲染:🔔 3
// unreadCount = 0 → 渲染:🔔(没有红点)
&&的工作原理(JS 短路求值):
A && B:如果 A 是true,返回 B;如果 A 是false,返回 A(B 不会执行)- 因此
unreadCount > 0 && <span>...</span>:只有条件成立时才渲染<span>
⚠️ 陷阱警告:当条件值是数字时要注意!
// ❌ 危险写法:当 count = 0 时,页面会渲染出一个 "0" { count && <p>有内容</p>; } // ✅ 安全写法:显式转为布尔值 { count > 0 && <p>有内容</p>; }
方式三:三元运算(适合二选一)
function ToggleSwitch({ isOn }) {
return (
<div>
{/* 三元运算:在 JSX 内部直接二选一 */}
<button className={isOn ? "btn-on" : "btn-off"}>
{isOn ? "开启" : "关闭"}
</button>
</div>
);
}
三种方式对比速查
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
if-else |
多分支、复杂逻辑 | 逻辑清晰,可读性强 | 只能在 return 外使用 |
&& |
显示/隐藏 | 写法极简 | 注意数字 0 的陷阱 |
三元 ?: |
二选一 | 一行搞定 | 嵌套太深可读性差 |
1.5 列表渲染与 key 属性
类比:图书馆的书架编号
想象你在图书馆管理一个书架。每本书都有一个唯一的索书号。当你需要找一本书、移动一本书、或者替换一本书时,你只需要根据索书号精准定位——而不是把整个书架的书都重新检查一遍。
React 中的 key 属性 就是每本书的"索书号"。
基础列表渲染
function TodoList() {
// 待办事项数据
const todos = [
{ id: 1, text: "学习 React", completed: false },
{ id: 2, text: "写作业", completed: true },
{ id: 3, text: "运动30分钟", completed: false },
];
return (
<ul>
{/* map 遍历数组,为每个元素生成对应的 JSX */}
{todos.map((todo) => (
// ⚠️ key 属性必须加在 map 返回的最外层元素上
<li
key={todo.id} // 用每条数据的唯一 id 作为 key
style={{
textDecoration: todo.completed ? "line-through" : "none",
}}
>
{todo.text}
</li>
))}
</ul>
);
}
// 渲染结果:
// · 学习 React
// · 写作业(带删除线)
// · 运动30分钟
key 属性的三个核心要点
1. key 是给 React 内部用的,不会出现在页面上
key 类似于数据库的主键——React 用它来追踪每个元素的身份,但你在浏览器 DevTools 里看不到它。
2. key 必须是"稳定、唯一、可预测"的
// ✅ 最佳实践:使用数据中自带的唯一 ID
todos.map((todo) => <li key={todo.id}>{todo.text}</li>);
// ✅ 次优方案:使用有业务含义的唯一值(如用户名、邮箱等)
users.map((user) => <li key={user.email}>{user.name}</li>);
// ❌ 万不得已才用的方案:使用 index
todos.map((todo, index) => <li key={index}>{todo.text}</li>);
3. key 只在兄弟节点之间需要唯一,全局不需要
function App() {
return (
<div>
<ul>
<li key="a">苹果</li> {/* 这里的 key="a" */}
<li key="b">香蕉</li> {/* 只在和 "苹果" 比较时需要唯一 */}
</ul>
<ul>
<li key="a">红色</li> {/* 这里的 key="a" 和上面的不冲突 */}
<li key="b">黄色</li> {/* 因为它们在另一个<ul>范围内 */}
</ul>
</div>
);
}
为什么不能用 index 作为 key?
这是一个非常经典的问题。用"去餐厅吃饭"来打比方:
情景:你的座位号就是你的点菜依据。服务员按座位号送菜。
- 用 学号(唯一 ID) 作为依据:你换到任何位置,服务员都能根据学号找到你,不会送错。
- 用 座位号(index) 作为依据:你从 3 号座换到 1 号座,服务员还把菜送到 3 号座,你就吃不到了。
// 模拟一个场景:列表项可以增删改排序
function BadExample() {
const [items, setItems] = useState([
{ id: "a", text: "第一项" },
{ id: "b", text: "第二项" },
]);
// ❌ 用 index 做 key
return (
<ul>
{items.map((item, index) => (
<li key={index}>
<input defaultValue={item.text} /> {/* 带输入框的列表项 */}
{item.text}
</li>
))}
</ul>
);
}
// 当你删除第一项后:
// 原来 index=0 的 "第一项" 消失了
// 原来 index=1 的 "第二项" 变成了 index=0
// React 看到 key=0 还在,以为还是原来的组件,不会重新渲染输入框
// 结果:输入框里的内容会发生错乱!
// ✅ 用唯一 ID 做 key 就没有这个问题
function GoodExample() {
const [items, setItems] = useState([
{ id: "a", text: "第一项" },
{ id: "b", text: "第二项" },
]);
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{" "}
{/* 用稳定的唯一 ID */}
<input defaultValue={item.text} />
{item.text}
</li>
))}
</ul>
);
}
// 删除 "第一项" 后:
// key='a' 的组件被彻底移除,key='b' 的组件保持不变
// 输入框内容不会错乱
总结 key 的正确取值优先级:
- 🥇 数据本身就有的唯一 ID(如数据库主键
item.id)- 🥈 从数据中组合出的唯一值(如
item.name + item.email)- 🥉 用
crypto.randomUUID()或nanoid生成唯一 ID 存入数据- 🚫 实在没有办法才用 index,但仅限于列表不会发生增删重排的静态列表
1.6 JSX 的编译原理
JSX 到底变成了什么?
很多新手以为 JSX 是 HTML、是字符串、是模板。都不是。JSX 经过编译后,会变成纯粹的 JavaScript 函数调用。
类比:翻译机的工作原理
想象你把中文输入翻译机,它会把中文转成英文。类似地,Babel(JSX 翻译机) 会把 JSX 语法转成浏览器能直接执行的 JavaScript。
编译前后对比
编译前——你写的 JSX:
const element = (
<div className="greeting">
<h1>你好,{name}!</h1>
<p>欢迎学习 React</p>
</div>
);
编译后——Babel 转换成的 JavaScript:
// JSX 每个标签都会被转换为 React.createElement() 调用
const element = React.createElement(
"div", // 参数1:标签类型(HTML标签名或组件名)
{ className: "greeting" }, // 参数2:属性对象(props)
React.createElement(
// 参数3...:子元素(可以无限嵌套)
"h1",
null, // h1 没有属性,传 null
"你好,", // 文本子节点
name, // 变量子节点
"!",
),
React.createElement(
// 另一个子元素
"p",
null,
"欢迎学习 React",
),
);
React.createElement 到底创建了什么?
// React.createElement 的返回值是一个普通的 JS 对象(称为"虚拟 DOM")
{
type: 'div',
props: {
className: 'greeting',
children: [
{
type: 'h1',
props: {
children: ['你好,', '小明', '!']
}
},
{
type: 'p',
props: {
children: '欢迎学习 React'
}
}
]
}
}
从 JSX 到页面的完整流程
你写的 JSX 代码
│
▼
[Babel 编译]
│
▼
React.createElement() 调用
│
▼
虚拟 DOM 对象(JS 对象树)
│
▼
[React 调和算法 (Reconciliation)]
│
▼
真实 DOM 更新(浏览器渲染)
核心理解:JSX 不是什么神奇的黑魔法,它只是
React.createElement()的语法糖。你写的每一行 JSX,最终都会被转成 JS 函数调用。所以 JSX 就是 JavaScript,不是 HTML,不是字符串,不是模板。
第二部分:React 严格模式
2.1 严格模式是什么?—— 代码质检工具
类比:驾校教练 vs 实际上路
- 实际上路(生产环境):只要你能把车开走、不出事故,没人管你是不是握方向盘姿势不对、是不是忘打转向灯。
- 驾校教练(严格模式):教练会严格盯着你的每一个动作——"你刚才并线怎么没看后视镜?""转弯速度太快了!""停车没拉手刹!"
React.StrictMode(严格模式)就是一个"驾校教练":
- 它只在**练习场(开发环境)**里起作用
- 它会指出你不规范的写法
- 到了正式上路(生产环境),它自动消失,不会影响性能
// 严格模式的基本使用
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
{" "}
{/* 👈 严格模式包裹 */}
<App />
</React.StrictMode>,
);
// 在这个包裹范围内的所有组件都会受到严格检查
一句话总结:严格模式 = 开发环境下的"代码质检工具",帮你提前发现潜在的 Bug。
2.2 严格模式的检查能力
检查一:识别过时的生命周期 API
React 从诞生到现在经历了多次版本迭代,一些早期的生命周期方法已经被标记为不安全(unsafe)。严格模式会用控制台警告提醒你。
// ❌ 问题代码:使用了过时的生命周期
class OldComponent extends React.Component {
componentWillMount() {
// ⚠️ 已废弃!
console.log("组件将要挂载");
}
componentWillReceiveProps() {
// ⚠️ 已废弃!
console.log("组件将要接收新的 props");
}
componentWillUpdate() {
// ⚠️ 已废弃!
console.log("组件将要更新");
}
render() {
return <div>旧版组件</div>;
}
}
// 严格模式下的控制台警告:
// Warning: componentWillMount has been renamed, and is not recommended for use.
为什么这些方法被废弃?
简单说,React 的未来版本(如 React 18+ 的并发特性)可能会多次调用这些生命周期方法,而不是只调用一次。如果你的 componentWillMount 里发了网络请求,就可能发出多次重复请求——造成数据错乱。
// ✅ 修复方案:使用新的生命周期方法或 Hooks
// 类组件修复
class NewComponent extends React.Component {
componentDidMount() {
// ✅ 使用 DidMount 替代 WillMount
console.log("组件已挂载");
// 这里发网络请求是安全的
}
componentDidUpdate(prevProps) {
// ✅ 使用 DidUpdate
console.log("组件已更新");
}
render() {
return <div>新版组件</div>;
}
}
// 函数组件 + Hooks 修复(现代推荐写法)
function ModernComponent() {
useEffect(() => {
console.log("组件已挂载");
// 这里发网络请求是安全的
}, []);
return <div>现代组件</div>;
}
检查二:检测不安全的遗留写法
严格模式会检测一些已经不推荐使用的 API。
// ❌ 问题代码:使用过时的 findDOMNode
class OldRefDemo extends React.Component {
myRef = React.createRef();
componentDidMount() {
// findDOMNode 是同步 API,在并发模式下可能出问题
const node = ReactDOM.findDOMNode(this); // ⚠️ 过时 API
node.style.color = "red";
}
render() {
return <div>旧版 Ref 用法</div>;
}
}
// 严格模式警告:
// findDOMNode is deprecated in StrictMode.
// ✅ 修复方案:使用 createRef 或 useRef
function ModernRefDemo() {
const divRef = useRef(null); // ✅ 用 useRef 创建引用
useEffect(() => {
if (divRef.current) {
divRef.current.style.color = "red"; // 直接通过 ref 访问 DOM
}
}, []);
return <div ref={divRef}>新版 Ref 用法</div>;
}
// ❌ 问题代码:使用过时的 Context API
import React from "react";
// 旧的 context 类型声明方式
class OldContextProvider extends React.Component {
// 旧版 contextTypes 已不推荐
static childContextTypes = {
theme: PropTypes.string,
};
getChildContext() {
// ⚠️ 旧版 Context API
return { theme: "dark" };
}
render() {
return <div>{this.props.children}</div>;
}
}
// ✅ 修复方案:使用新版 Context API
const ThemeContext = React.createContext("light"); // 新版 Context
function NewContextProvider({ children }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
function ThemedButton() {
const theme = useContext(ThemeContext); // 用 Hook 读取
return <button className={theme}>主题按钮</button>;
}
检查三:检测意外的副作用重复执行
这是严格模式最重要的检查能力。它会在开发环境下故意重复调用某些函数,看看你的代码是不是"经得起反复折腾"。
// ❌ 问题代码:在渲染阶段执行副作用
function BadComponent({ userId }) {
// 在组件函数体中直接发请求——这是大忌!
fetch(`/api/user/${userId}`) // ⚠️ 渲染阶段有副作用
.then((res) => res.json())
.then((data) => {
// 这种写法会导致不可预测的问题
});
// 在函数体中直接修改外部变量——也是大忌!
localStorage.setItem("lastUserId", userId); // ⚠️ 副作用
return <div>用户 ID:{userId}</div>;
}
// 严格模式会故意渲染两次,上面的 fetch 会发两次请求,localStorage 会写入两次!
// ✅ 修复方案:把副作用放到 useEffect 中
function GoodComponent({ userId }) {
useEffect(() => {
// 副作用正确地放在了 useEffect 里
fetch(`/api/user/${userId}`)
.then((res) => res.json())
.then((data) => {
// 处理数据
});
localStorage.setItem("lastUserId", userId);
}, [userId]); // 依赖 userId,它变化时才重新执行
return <div>用户 ID:{userId}</div>;
}
2.3 副作用"二次执行"的秘密
这是 React 18+ 严格模式最特别的行为,也是很多初学者会困惑的地方。
现象:开发环境下代码好像"跑了两遍"
function DoubleRunDemo() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("useEffect 执行了"); // 开发环境下会打印两次!
console.log("count =", count);
return () => {
console.log("清理函数执行了"); // 也会打印!
};
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
// 开发环境控制台输出:
// useEffect 执行了
// count = 0
// 清理函数执行了 ← 注意:React 先执行清理
// useEffect 执行了 ← 然后再执行一次 Effect
// count = 0
// ✅ 生产环境只输出一次:
// useEffect 执行了
// count = 0
为什么要故意执行两次?
类比:战斗机的双引擎检查
战斗机起飞前,地勤人员会逐一检查两个引擎——关掉一个,看另一个能不能正常工作。如果关掉一个引擎另一个就熄火,说明设计有问题。
React 也是这个思路:它先挂载→卸载→再挂载,相当于模拟了一次"拔掉电源再插上"。如果你的组件在这个过程中崩溃了,说明你的副作用代码没有正确处理"卸载"。
实际案例:未清理的事件监听
// ❌ 问题代码:没有返回清理函数
function BadListener() {
useEffect(() => {
// 添加了窗口滚动监听
window.addEventListener("scroll", handleScroll);
// ⚠️ 但是忘记在组件卸载时移除!内存泄漏!
}, []); // 没有返回清理函数
const handleScroll = () => {
console.log("页面滚动了");
};
return <div style={{ height: "200vh" }}>滚动页面测试</div>;
}
// 严格模式:挂载→卸载→再挂载
// 第一次挂载:添加了 1 个监听器
// 卸载时:没有移除!监听器还在!
// 第二次挂载:又添加了 1 个监听器
// 结果:实际上有 2 个监听器在跑,handleScroll 被调用了两次!
// ✅ 正确代码:返回清理函数
function GoodListener() {
useEffect(() => {
const handleScroll = () => {
console.log("页面滚动了");
};
window.addEventListener("scroll", handleScroll);
// ✅ 返回一个清理函数——组件卸载时 React 会自动调用它
return () => {
window.removeEventListener("scroll", handleScroll);
console.log("已移除滚动监听");
};
}, []);
return <div style={{ height: "200vh" }}>滚动页面测试</div>;
}
// 严格模式执行流程:
// 挂载 → 添加监听器 1
// 卸载 → 清理函数执行,移除监听器 1 ✅
// 再挂载 → 添加监听器 2
// 最终只有 1 个监听器在运行 ✅
实际案例:未清理的定时器
// ❌ 问题代码:定时器泄露
function BadTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount((c) => c + 1); // ⚠️ 每1秒+1,但组件卸载后定时器还在跑!
}, 1000);
// ⚠️ 没有清理定时器——即使离开页面,计数器仍在后台运行
}, []);
return <div>计数:{count}</div>;
}
// 用户离开页面后,setInterval 依然在运行,造成内存泄漏和无效渲染
// ✅ 正确代码:清理定时器
function GoodTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timerId = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
// ✅ 清理函数中清除定时器
return () => {
clearInterval(timerId);
console.log("已清除定时器");
};
}, []);
return <div>计数:{count}</div>;
}
严格模式会触发两次调用的函数
| 函数 | 调用次数(开发环境) | 目的 |
|---|---|---|
useState 的初始化函数 |
2 次 | 检测纯函数 |
useReducer 的 reducer |
2 次 | 检测纯函数 |
useEffect |
挂载→卸载→再挂载 | 检测清理逻辑 |
useLayoutEffect |
挂载→卸载→再挂载 | 检测清理逻辑 |
useMemo / useCallback |
可能多调一次 | 检测纯函数 |
| 组件函数体 | 2 次 | 检测纯渲染 |
⚠️ 重要:这些"二次执行"只在开发环境发生。在生产环境中,所有这些函数都只执行一次。所以不用担心性能问题——它只是一个开发辅助工具。
2.4 严格模式的使用方式
方式一:包裹整个应用(推荐)
最常见的用法——在入口文件把整个应用包起来。
// src/index.js —— 应用入口文件
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
// 效果:App 组件及其所有子组件都会受到严格检查
适用场景:
- 大多数项目都推荐这种用法
- 全局统一检查,不会遗漏任何组件
- 团队开发时强制所有人都遵守规范
方式二:局部包裹部分组件
只在某些特定组件上使用严格模式——就像只给某个区域装监控摄像头。
// 在整个应用中,只想对特定部分进行严格检查
function App() {
return (
<div>
<Header /> {/* 不受严格检查 */}
{/* 只对 Dashboard 部分做严格检查 */}
<React.StrictMode>
<Dashboard /> {/* 受严格检查 */}
</React.StrictMode>
<Footer /> {/* 不受严格检查 */}
</div>
);
}
适用场景:
- 逐步迁移老旧项目,不能一次性全部开启严格模式
- 怀疑某个特定模块有问题,集中排查
- 第三方组件库不兼容严格模式时,可以局部排除
方式三:嵌套多个 StrictMode(效果叠加)
function App() {
return (
<React.StrictMode>
<Header />
{/* 嵌套不会造成问题,但也没必要——一个就够了 */}
<React.StrictMode>
<Dashboard />
</React.StrictMode>
<Footer />
</React.StrictMode>
);
}
// 效果和方式一一样,嵌套不会产生额外影响
第三部分:初学者常见踩坑汇总
| 序号 | 常见错误 | 错误原因 | 正确写法 |
|---|---|---|---|
| 1 | 写了 class="xxx" |
class 是 JS 关键字 |
用 className="xxx" |
| 2 | 写了 for="xxx" |
for 是 JS 关键字 |
用 htmlFor="xxx" |
| 3 | JSX return 多行时不加括号 | JS 自动分号插入机制会把 return 后的换行当成语句结束 | 用 return ( 开括号,) 闭括号 |
| 4 | {} 里写 if / for |
if 和 for 是语句,不是表达式 |
用三元运算 ?: 或逻辑与 && |
| 5 | style="color:red" |
JSX 的 style 必须用对象 | style={{ color: 'red' }} |
| 6 | 忘记写 key 属性 |
React 需要 key 来追踪列表变化 | 给每个 map 的最外层 JSX 加 key={唯一值} |
| 7 | 用 index 做 key |
列表增删时会导致渲染错乱 | 用数据中的唯一 ID 做 key |
| 8 | <br> 不闭合 |
JSX 严格要求标签闭合 | 写 <br /> |
| 9 | 事件写 onclick={handleClick()} |
加 () 会在渲染时立刻执行函数 |
写 onClick={handleClick} 不加括号 |
| 10 | 在组件函数体中发请求 | 渲染阶段不应有副作用 | 把请求放到 useEffect 中 |
| 11 | useEffect 不写依赖数组 |
每次渲染都执行,影响性能 | 根据需求写 [] 或 [dep1, dep2] |
| 12 | 写 useEffect 不返回清理函数 |
事件监听、定时器会泄漏 | 返回清理函数 return () => { cleanup } |
| 13 | 以为严格模式影响生产性能 | 不了解严格模式只在开发环境生效 | 放心使用,生产环境自动关闭 |
| 14 | 注释写在 JSX 子元素位置用 // |
JSX 中 // 会被当成文本渲染 |
用 {/* 注释 */} 格式 |
| 15 | 多个 JSX 元素并列返回 | 必须有单一根节点 | 用 <div> 或 <>...</> 包裹 |
第四部分:配套练习题与答案
练习题
第 1 题:JSX 语法纠错
下面的 JSX 代码中有 5 处错误,请找出来并写出正确的代码。
function UserCard() {
const user = { name: '小红', age: 25, role: 'admin' };
return
<div class="card">
<img src="avatar.jpg">
<h1>用户:user.name</h1>
<p>年龄:{user.age}</p>
<label for="role">角色:</label>
</div>
<p>底部信息</p>
}
第 2 题:表达式嵌入
请补全下面的代码,实现:当 score >= 60 时显示"及格 ✅",否则显示"不及格 ❌"。
function ExamResult({ score }) {
return (
<div>
<h2>考试成绩</h2>
{/* 请在下面补全代码 */}
<p>结果:__________</p>
</div>
);
}
第 3 题:列表与 key
下面的代码有两个问题,请修改。
function StudentList() {
const students = ["张三", "李四", "王五"];
return (
<ul>
{students.map((name, i) => {
<li key={i}>{name}</li>;
})}
</ul>
);
}
第 4 题:严格模式的作用
关于 React.StrictMode 的下列说法,哪些是正确的?(多选)
A. 严格模式在生产环境下会自动关闭,不影响性能
B. 严格模式会让 useEffect 在生产环境下执行两次
C. 严格模式可以检测过时的生命周期方法
D. 严格模式能阻止代码中的所有 Bug
E. 严格模式下,useEffect 中忘记清理定时器会更早暴露问题
第 5 题:条件渲染实战
请补全下面的代码,实现:
- 如果
loading为true,显示"加载中..." - 如果
loading为false且error不为空,显示错误信息 - 如果
loading为false且error为空,显示数据列表
function DataDisplay({ loading, error, data }) {
// 请在下方完成代码
return <div>{/* 你的代码 */}</div>;
}
参考答案
第 1 题答案
5 处错误:
return后没有用括号包裹多行 JSX(会导致什么都不返回)class应为className<img>标签没有闭合(缺少/>或</img>)user.name没有放在{}里面(会被当成纯文本 "user.name")for应为htmlFor- 两个并列的
<div>和<p>没有用根元素包裹(这算是第6处,回答5处即可)
正确代码:
function UserCard() {
const user = { name: "小红", age: 25, role: "admin" };
return (
<>
{" "}
{/* 用 Fragment 包裹多个元素 */}
<div className="card">
{" "}
{/* class → className */}
<img src="avatar.jpg" alt="头像" /> {/* 标签必须闭合 */}
<h1>用户:{user.name}</h1> {/* user.name 放入 {} */}
<p>年龄:{user.age}</p>
<label htmlFor="role">角色:</label> {/* for → htmlFor */}
</div>
<p>底部信息</p>
</>
);
}
第 2 题答案
function ExamResult({ score }) {
return (
<div>
<h2>考试成绩</h2>
<p>结果:{score >= 60 ? "及格 ✅" : "不及格 ❌"}</p>
{/* 三元运算是最简洁的二选一方案 */}
{/* 也可以用 && 分两行:score >= 60 && <p>及格 ✅</p> */}
</div>
);
}
第 3 题答案
两个问题:
map回调函数用了{}但没有return,应该用()或加return- 用
index作为 key 不够安全(虽然这里是个简单的静态数组,但教学上应该指导使用更稳定的 key)
function StudentList() {
const students = [
{ id: 1, name: "张三" },
{ id: 2, name: "李四" },
{ id: 3, name: "王五" },
];
return (
<ul>
{students.map((student) => (
// ✅ 用 () 隐式返回(箭头函数的简写)
// ✅ 用 student.id 作为 key,而非 index
<li key={student.id}>{student.name}</li>
))}
</ul>
);
}
第 4 题答案
正确答案:A、C、E
- A ✅ 正确:严格模式只在开发环境生效,生产环境自动忽略
- B ❌ 错误:useEffect 二次执行只在开发环境,生产环境只执行一次
- C ✅ 正确:严格模式会检测 componentWillMount 等过时生命周期并给出警告
- D ❌ 错误:严格模式只是一套警告机制,能帮你发现潜在问题,但不能阻止所有 Bug
- E ✅ 正确:因为开发环境下会"挂载→卸载→再挂载",如果没清理定时器,会更容易暴露问题
第 5 题答案
function DataDisplay({ loading, error, data }) {
// 方案:在 return 之前用 if 判断不同状态
if (loading) {
return (
<div className="status-message">
<p>加载中...</p>
</div>
);
}
if (error) {
return (
<div className="error-message">
<p>出错了:{error}</p>
</div>
);
}
// loading 为 false 且没有 error → 正常显示数据
return (
<div>
<h2>数据列表</h2>
<ul>
{data.map((item) => (
<li key={item.id}>
{item.name} — {item.value}
</li>
))}
</ul>
</div>
);
}
学习建议:学完本文后,建议动手把每个代码示例敲一遍,亲眼看一看严格模式下的控制台警告是什么样子,加深理解。JSX 和严格模式是 React 入门的基石,掌握好它们,后面的学习会顺利很多。
评论区