目 录CONTENT

文章目录

React(二)JSX语法与严格模式

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 + 2user.namearr.map()condition ? a : b ✅ 能
语句 执行一个动作,不产生值 ifforwhileswitchconst 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 的正确取值优先级

  1. 🥇 数据本身就有的唯一 ID(如数据库主键 item.id
  2. 🥈 从数据中组合出的唯一值(如 item.name + item.email
  3. 🥉 用 crypto.randomUUID()nanoid 生成唯一 ID 存入数据
  4. 🚫 实在没有办法才用 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 iffor 是语句,不是表达式 用三元运算 ?: 或逻辑与 &&
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 题:条件渲染实战

请补全下面的代码,实现:

  • 如果 loadingtrue,显示"加载中..."
  • 如果 loadingfalseerror 不为空,显示错误信息
  • 如果 loadingfalseerror 为空,显示数据列表
function DataDisplay({ loading, error, data }) {
  // 请在下方完成代码

  return <div>{/* 你的代码 */}</div>;
}

参考答案

第 1 题答案

5 处错误

  1. return 后没有用括号包裹多行 JSX(会导致什么都不返回)
  2. class 应为 className
  3. <img> 标签没有闭合(缺少 /></img>
  4. user.name 没有放在 {} 里面(会被当成纯文本 "user.name")
  5. for 应为 htmlFor
  6. 两个并列的 <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 题答案

两个问题

  1. map 回调函数用了 {} 但没有 return,应该用 () 或加 return
  2. 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 入门的基石,掌握好它们,后面的学习会顺利很多。

0

评论区