React 避坑指南:“闭包陷阱“

写在前面:如果你是 React 新手,或者刚从 Class 组件转到 Hooks,这篇文章或许可以帮你省下几根头发。
广告植入:欢迎访问我的个人网站:https://hixiaohezi.com


案发现场

事情发生在两年前的一个周五下午(是的,墨菲定律通过不缺席),当时我正在写一个极其简单的功能:倒计时

需求很简单:用户点击按钮开始倒计时 60 秒,每秒更新页面上的数字,倒计时结束后自动重置。

作为一名老菜鸟,我脑子一扔,直接敲下了如下代码:

tsx 复制代码
function Timer() {
  const [count, setCount] = useState(60);

  useEffect(() => {
    const timer = setInterval(() => {
      console.log('当前倒计时:', count); // 用于调试
      
      if (count > 0) {
        setCount(count - 1); // 逻辑很完美对吧?
      } else {
        clearInterval(timer);
      }
    }, 1000);

    // 清理定时器
    return () => clearInterval(timer);
  }, []); // 空依赖数组,因为我只想让定时器启动一次

  return <div>倒计时:{count} 秒</div>;
}

我保存了代码,刷新了浏览器,准备提交代码然后下班跑路。

诡异的现象

屏幕上的数字从 60 变成了 59

然后... 就定格在了 59

但我打开控制台一看,控制台在疯狂输出:

复制代码
当前倒计时: 60
当前倒计时: 60
当前倒计时: 60
...

What???

我的 count 已经变成 59 了啊(页面都变了),为什么 setInterval 打印出来的还是 60?为什么它一直在重复 60 - 1 = 59 这个动作,却再也下不去了?

我当时的第一反应是:

  1. React 坏了?
  2. 浏览器坏了?
  3. 宇宙射线干扰了 CPU?

唯独没想过是自己菜。

破案:该死的"闭包"

在对着屏幕发呆了 n 分钟,查阅了无数 StackOverflow 之后,我终于明白了真相。

这个坑的名字叫:Stale Closure (陈旧闭包 / 僵尸闭包)

简单用人话解释一下:

当你写下 useEffect(..., []) 且依赖数组为空时,这个 Effect 只会在组件挂载时执行一次

这时候,setInterval 被创建了。它捕获了当时 环境下的 count 变量。
当时count 是 60。

无论组件后来重新渲染了多少次,无论页面上的 count 变成了 59、58 还是 0,定时器里的那个回调函数,依然是第一次创建时的那个函数

在那个函数"冻结"的记忆里,count 永远是 60。

所以它每一秒都在做同一件事:

"噢,现在 count 是 60,我要把它变成 59。"

像不像一条只有 7 秒记忆的鱼?

解决方案

找到了原因,解决就很简单了。这里提供两种方案,但我强烈推荐第一种。

方案一:函数式更新 (推荐 ✅)

这是最优雅的解法,不需要重置定时器。

tsx 复制代码
useEffect(() => {
  const timer = setInterval(() => {
    // 重点在这里!!!
    // prevCount 是 React 传进来的最新值,不依赖外部闭包
    setCount((prevCount) => {
      if (prevCount <= 1) {
        clearInterval(timer);
        return 0;
      }
      return prevCount - 1;
    });
  }, 1000);

  return () => clearInterval(timer);
}, []); // 依然可以是空数组

为什么有效? :因为 setState 如果接收一个函数,React 会保证把最新的 状态值传给你。你不需要读取外部的 count 变量,从而绕过了闭包陷阱。

方案二:useRef 大法 (万能 ✅)

如果你不仅要更新状态,还要在定时器里读取 最新的 props 或其他状态做判断,useRef 是救命稻草。

tsx 复制代码
const countRef = useRef(count);

// 每次渲染都更新 ref.current
useEffect(() => {
  countRef.current = count;
}, [count]);

useEffect(() => {
  const timer = setInterval(() => {
    // ref.current 永远是最新的,因为它是一个引用对象
    if (countRef.current > 0) {
      // do something...
    }
  }, 1000);
}, []);

如果这个坑你也踩过,握个爪。

最后,再加个广告,欢迎访问我的个人网站:👉 hixiaohezi.com

(这里是我的个人网站,虽然没有 Hooks 那么复杂,但绝对真诚)

祝大家的代码永远没有 Bug,只有 Feature!

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax