React Hooks
闭包陷阱(Closure Trap)
闭包陷阱在 React Hooks 或 JavaScript 中常见,尤其是在定时器、事件监听等异步回调里
tsx
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log(count); // ✨ 每次 log 0
setCount(count + 1);
}, 1000);
}, []);
return <div>{count}</div>; // ✨ 从0变为1 后续只会显示1
}我们知道每次触发 hook 都会被hook重新执行 App()
此处:
- useEffect首次渲染时执行了创建定时器,完成执行 App(), 得到状态0
- 1s 后触发 useState,hook触发重新执行 App(),得到状态 0+1
- 因为useEffect依赖数组是[],因此不会重新执行useEffect
- 1s 后内部继续出发useSatte,hook触发重新执行App(),因useEffect内的状态不会随着多次的重新执行App()而得到更新,而始终是初次执行useEffect时的状态,得到状态 0+1
解释
👇 理想状态下定时器内的 count + 1可以获取最新每次最新的状态,如下图:

👇 然而从代码的执行上看: count 状态永远是第一次执行 App() 时的状态 0

👇 这个现象是闭包陷阱:
- useEffect 只在初次渲染时执行一次(因为依赖数组是 [])。
- setInterval 里的回调函数“记住”了当时的 count(比如 0),之后每次执行都用的是这个旧值。
- 所以每次 setCount(count + 1) 实际上都是 setCount(0 + 1),count 只会变成 1,后面不会再变。
JS原理:
假设我们想每秒打印一次 count,并让 count 自增:
js
let count = 0;
setInterval(function () {
count++;
}, 1000);上面这段代码没问题,每秒都会打印递增的 count。
如果我们把 count 放到一个函数作用域里,并用闭包访问它:
js
function main() {
let count = 0;
setInterval(function () {
console.log(count); // 闭包捕获了 count
count++;
}, 1000);
}
main();这也没问题,因为闭包每次都能访问到最新的 count。
👇 闭包陷阱的本质:问题出现在“闭包捕获的是某个时刻的变量值”,而不是“变量的最新值”
js
function main() {
let count = 0;
function useEffect(capturedCount) {
setInterval(() => {
console.log(capturedCount, count); // 定时器每次取值只能取 capturedCount 0
count = capturedCount + 1; // 即使允许set外部的值,但只能取 capturedCount 来计算
}, 1000);
}
useEffect(count); // 传入 count 状态,只允许内部从当前params取值,允许在内部set外部的值,但是不会拿到外部最新的值,只能拿到这个params的状态
}
main();useEffect 内 get的状态是由外部传入的,只会是初次执行时的状态,在内部我们命名为capturedCount,useEffect不能拿到最外层实时的值,但是可以set。
👇 因此定时器内部获取 count 不能直接取外部(如果可以取外部则状态是最新的),而是从 function params 中取,然后成功set外部的状态,因此会体现为:
0 0
0 1
0 1
...React 中因为 useEffect 只在初次渲染时执行一次,setInterval 里的回调函数“记住”了当时的 count(0),之后每次执行(get值来运算)都用的是这个旧值
闭包陷阱(Closure Trap)解决办法
不形成闭包
不让它形成闭包不就行了?
这时候可以用 setState 的另一种参数:
TODO: 补充 react 文档链接
tsx
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log(count); // ✨ 每次 log 0
// setCount(count + 1);
setCount((count) => count + 1); // 使用回调函数,而不是取params中的状态,而是由 React 调用这个回调函数获取一个最新的状态
}, 1000);
}, []);
return <div>{count}</div>; // ✨ 正确自增
}