React 严格模式(Strict Mode):双重挂载检测对定位组件副作用(Side Effects)的工程价值

大家好,欢迎来到今天的讲座。

今天我们不谈那些花里胡哨的 Hooks 新特性,也不聊 Next.js 的部署姿势。我们要聊一个听起来像是个“强迫症诊所”的玩意儿——React 严格模式。

我知道,很多同学在开发中看到 <React.StrictMode> 这玩意儿,第一反应是:“这货是干嘛的?是不是嫌我代码写得太烂,特意来嘲讽我的?”

别急,别急。今天我们就把 React 严格模式像剥洋葱一样剥开,看看它那看似神经质的行为背后——也就是所谓的“双重挂载检测”——到底能给我们的工程化带来什么实实在在的价值。

我们要聊的核心问题是:为什么 React 要在开发环境下把你的组件“杀掉”再“复活”?这到底是恶作剧,还是为了救你的命?

好,让我们直接切入正题。

一、 严格模式:React 的“严厉老师”

首先,我们要纠正一个观念。React 严格模式(Strict Mode)不是一个错误检查器,它不像 ESLint 那样会直接指着你的鼻子说“这里有个未定义的变量”。

严格模式更像是一个严厉的体育老师。它不会因为你跑得慢就罚你跑圈,但它会要求你把动作做两遍

它的主要作用是:

  1. 检测不安全的生命周期方法。
  2. 检测过时的 API 使用。
  3. 检测副作用(Side Effects)

而我们要重点讲的,就是最后这一点。为了让你理解这个“副作用检测”,我们需要先搞清楚 React 到底在搞什么鬼。

二、 “双重挂载”:当你的组件被“凌迟”

在 React 的世界里,组件的生命周期通常是这样的:

  1. 挂载:组件进入 DOM,useEffect 执行。
  2. 更新:props 变了,组件重新渲染,useEffect 再次执行。
  3. 卸载:组件离开 DOM,useEffect 执行清理函数。

这听起来很顺理成章,对吧?就像你签了一纸婚约,领了证,然后过日子。

但是,React 严格模式在开发环境下,会强制把你的这个流程改成这样:

  1. 挂载:组件进入 DOM,useEffect 执行。
  2. 卸载:组件立刻被踢出 DOM,useEffect 的清理函数执行。
  3. 再次挂载:组件重新进入 DOM,useEffect 再次执行。
  4. 再次卸载:组件再次被踢出,清理函数再次执行。

等等,啥?

你可能会问:“这不是精神分裂吗?这就像你刚结婚,民政局刚给你盖章,下一秒离婚证就下来了,然后再领一次证?这日子还过不过了?”

这就是严格模式的“双重挂载”。它模拟了组件被意外销毁并重建的场景。它为什么要这么做?为了干什么?为了检测你的副作用是否具有“幂等性”。

三、 什么是副作用?为什么它是个坑?

在 React 中,渲染函数通常被认为是“纯函数”。输入 props,输出 JSX。它不应该有副作用。就像做数学题,输入数字,输出结果,不能一边算一边去厨房煮面。

但是,现实世界是复杂的。我们的组件往往需要做点“杂事”:

  • 发送网络请求(fetchaxios)。
  • 订阅外部事件(socket.onsetInterval)。
  • 操作 DOM(document.titlelocalStorage)。
  • 调用第三方库。

这些就是副作用。它们发生在渲染之后,并且可能依赖于渲染的结果。

为什么 React 要管你这些杂事?因为 React 的核心哲学是可预测性。如果你在渲染过程中偷偷干了坏事,React 就很难追踪数据流,很难优化性能,更难调试。

四、 代码示例:当副作用遇到严格模式

为了证明严格模式的价值,我们来写点代码。

场景一:一个“单相思”的 API 请求

假设你写了一个 UserList 组件,你想在组件挂载的时候去获取用户数据。

import React, { useState, useEffect } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 模拟 API 请求
    console.log("开始请求用户数据...");
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      });
  }, []); // 依赖数组为空,只执行一次

  if (loading) return <div>加载中...</div>;
  return <ul>{users.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

这段代码看起来很完美,对吧?组件挂载,请求发送,数据回来,渲染。

现在,我们在父组件里加上 <React.StrictMode>,然后运行。

发生了什么?

你会发现控制台输出了两行:
开始请求用户数据...
开始请求用户数据...

你的浏览器 Network 面板里,/api/users 请求了两次!

这很糟糕吗?
在开发环境下,这很烦人。但在生产环境下,如果你的 setUsers 是一个非幂等操作(比如增加计数器,或者更新数据库),那生产环境就会出大问题。你可能不小心给用户发了两封欢迎邮件。

严格模式的价值就在这里:它提前暴露了你的“懒惰”。

它强迫你思考:如果我的组件被卸载了,我正在进行的请求怎么办?我还需要发送第二次请求吗?

场景二:修正——学会说“再见”

为了解决这个问题,我们需要在 useEffect 里写清理函数。

useEffect(() => {
  console.log("开始请求用户数据...");
  const controller = new AbortController(); // 现代 Fetch API 的取消机制
  const signal = controller.signal;

  fetch('/api/users', { signal })
    .then(res => res.json())
    .then(data => {
      if (!signal.aborted) { // 再次检查,防止在清理函数执行后数据才回来
        setUsers(data);
        setLoading(false);
      }
    })
    .catch(err => {
      if (err.name !== 'AbortError') console.error(err);
    });

  // 清理函数:组件卸载时调用
  return () => {
    console.log("组件卸载,取消请求");
    controller.abort();
  };
}, []);

现在,当 React 严格模式触发“双重挂载”时,流程是这样的:

  1. 组件挂载,发起请求 A。
  2. 组件卸载,触发清理函数,请求 A 被取消。
  3. 组件再次挂载,发起请求 B。
  4. 组件再次卸载,触发清理函数,请求 B 被取消。

结果: 请求被取消,没有数据被错误地 setState。控制台虽然会有两行日志,但网络请求是干净利落的。

这就是严格模式的工程价值:它通过模拟极端情况(瞬间销毁重建),强制你编写健壮的清理逻辑。

五、 定时器与内存泄漏:被遗忘的幽灵

除了网络请求,副作用中还有一个著名的坑:setTimeoutsetInterval

如果你在 useEffect 里启动了一个定时器,却忘了在组件卸载时 clearTimeout,那这个定时器就会变成一个幽灵,继续在后台运行,继续更新已经销毁的组件状态。这就是经典的内存泄漏

严格模式对定时器的检测尤为“狠辣”。

function BadTimer() {
  useEffect(() => {
    const id = setInterval(() => {
      console.log("Tick tock...");
    }, 1000);

    // 忘了写清理函数!
  }, []);

  return <div>我是个定时器</div>;
}

加上严格模式后,你会看到控制台疯狂输出:
Tick tock...
Tick tock...
Tick tock...

每隔 0.5 秒(因为卸载再挂载),定时器就会重新启动。而且,由于没有清理,旧的定时器还在跑,新的定时器还在跑。你的控制台会瞬间爆满,CPU 占用飙升。

如果这发生在生产环境,你的后台可能会被你的定时器刷屏。

通过严格模式,我们被迫养成一个好习惯:useEffect 的返回值里写清理逻辑。 这不仅是为了 React,更是为了你的代码卫生。

六、 工程价值深度解析:为什么这很重要?

讲了这么多技术细节,我们到底在图什么?为什么作为一个资深的工程师,我们要重视这个“双重挂载检测”?

1. 破除“侥幸心理”

很多初级开发者写代码,是靠运气和经验主义的。
“嗯,这个组件看起来不会销毁,我就不写清理函数了。”
“嗯,这个 API 请求很快,就算重复发一次也没事。”

React 严格模式就是那个打破你“侥幸心理”的锤子。它通过强制重复执行,让你直观地看到如果不写清理函数会发生什么。这种“视觉上的恐怖”能让你深刻记住:副作用必须清理。

2. 提升生产环境的稳定性

这是最核心的工程价值。

React 的设计哲学之一是“一致性”。开发环境模拟生产环境的行为。

如果严格模式能帮你发现并修复一个潜在的内存泄漏,那你就在生产环境中救了你的服务器。想象一下,一个复杂的后台管理系统,如果每个页面都泄漏一个 1MB 的定时器,运行一周后,内存占用直接爆表,系统卡死。

严格模式是你在上线前的最后一道防线。

3. 帮助你理解组件的生命周期

很多时候,我们对组件的理解是模糊的。我们只知道“渲染”,不知道“卸载”。

严格模式通过 useEffect 的清理函数调用,让你亲身体验了“卸载”这一刻。你会开始思考:

  • 这个数据是在哪里产生的?
  • 它会在什么时候失效?
  • 如果组件突然销毁,我需要做什么来“收尾”?

这种思维模式的转变,是从“写脚本”到“写架构”的重要一步。

4. 优化代码结构

严格模式还能帮你发现那些不清晰的依赖关系。

如果你在 useEffect 里写了一些代码,但是没有把它们放在 useEffect 里,而是放在了组件的顶部,或者在条件语句里。严格模式虽然不能直接报错,但如果你在 useEffect 里用了未定义的变量,它会报错。

更重要的是,它迫使你思考:这个副作用到底应该依赖什么? 是空数组 [](只执行一次)?还是依赖某些 props?还是依赖 state?

这种对依赖关系的审视,能极大减少 Bug。

七、 进阶:严格模式下的“副作用陷阱”

让我们再深入一点。副作用不仅仅是 API 请求。有些副作用非常隐蔽,连严格模式都难以完全避免,这反而暴露了 React 设计的局限性。

场景:第三方库的副作用

假设你引入了一个旧的第三方库,这个库在初始化时会自动播放一段背景音乐。

useEffect(() => {
  // 这个库初始化时自动播放音乐
  oldLibrary.init({ autoPlay: true }); 
}, []);

在严格模式下,这个库会被初始化两次,音乐会播放两次。而且,当组件卸载时,这个库可能并没有提供清理函数来停止音乐。

这时候,你会非常抓狂。你可能会想:“React 严格模式真是个坑货,把我的音乐都搞乱了。”

这时候,你的工程价值体现出来了:你有能力去修改这个库,或者封装它。

你可以写一个 Wrapper 组件,在初始化前判断是否已经初始化过;或者在组件卸载时,强制调用库的销毁方法。这迫使你从“盲目集成”转变为“负责任的集成”。

场景:状态的不一致性

这是最棘手的情况。

假设你在 useEffect 里做了一个复杂的计算,这个计算依赖于 props,并且更新了 state。

function ComplexComponent({ data }) {
  const [result, setResult] = useState(null);

  useEffect(() => {
    // 复杂计算
    const res = heavyComputation(data);
    setResult(res);
  }, [data]); // 依赖 data

  return <div>{result}</div>;
}

在严格模式下,data 不变,所以 useEffect 不会执行。但是,组件会先卸载再挂载。这会导致 result state 被重置为 null

虽然这通常不会导致 Bug(因为渲染结果是一样的),但它破坏了 React 的“状态保持”特性。

这教会了我们什么?不要把逻辑放在 useEffect 里。 useEffect 是为了处理那些副作用,而不是为了处理业务逻辑。业务逻辑应该放在渲染函数里(如果依赖很少),或者放在 useReducer 里。

严格模式在这里充当了一个“反向导师”,告诉你:你的代码结构可能有问题。

八、 总结与实战建议

好了,讲了这么多,我们总结一下 React 严格模式的双重挂载检测到底给我们带来了什么工程价值。

  1. 它是你的“压力测试仪”:它模拟了组件的意外销毁和重建,检测你的清理函数是否健壮。
  2. 它是你的“Bug 捕手”:它能帮你发现内存泄漏、重复的 API 请求、未清理的定时器等隐形杀手。
  3. 它是你的“架构向导”:它强迫你区分渲染逻辑和副作用逻辑,让你写出更纯粹、更可预测的代码。

那么,作为资深工程师,我们应该怎么做?

  1. 拥抱它:在开发环境下,永远不要移除 <React.StrictMode>。把它当成你的护身符。
  2. 写好清理函数:这是基本功。记住 useEffect 返回的函数就是你的“分手信”。
  3. 区分逻辑与副作用:尽量把纯逻辑放在渲染函数或自定义 Hook 中,把脏活累活(API、定时器)放在 useEffect 并做好清理。
  4. 处理第三方库:对于不兼容严格模式的库,不要只是抱怨,要去封装它,解决它。

最后,我想说,React 严格模式虽然看起来有点“神经质”,有点“强迫症”,但它的初衷是好的。它试图在一个充满不确定性的开发环境中,给你提供一点点确定性。

代码不是写完就结束了,而是要维护、要演进、要健壮。而 React 严格模式,就是那个在背后默默帮你检查作业的严厉老师。虽然有时候会骂你,但最终,它会让你成为一个更优秀的程序员。

希望今天的讲座能让你对 React 严格模式有一个全新的认识。下次看到它触发双重挂载时,别慌,别骂,看看你的清理函数写对了没有。

谢谢大家。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注