写在前面:如果你是 React 新手,或者刚从 Class 组件转到 Hooks,这篇文章或许可以帮你省下几根头发。
广告植入:欢迎访问我的个人网站: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。
但我打开控制台一看,控制台在疯狂输出:
makefile
当前倒计时: 60
当前倒计时: 60
当前倒计时: 60
...
What???
我的 count 已经变成 59 了啊(页面都变了),为什么 setInterval 打印出来的还是 60?为什么它一直在重复 60 - 1 = 59 这个动作,却再也下不去了?
我当时的第一反应是:
- React 坏了?
- 浏览器坏了?
- 宇宙射线干扰了 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!