--- tags: [React, Hooks, useState, Closure, Frontend] create time: 2026-04-30 --- # 闭包陷阱(Closure Trap) ## 现象 ```tsx function Counter() { const [count, setCount] = useState(0); const handleClick = () => { // ❌ count 始终捕获定义时的 0 setTimeout(() => setCount(count + 1), 1000); // 即使用户在 1 秒内点了 5 次按钮,结果都是 1,而不是 5 }; return ; } ``` ## 结合组件渲染的生命周期拆解 > [!warning] 核心认知:React 每次渲染都创建全新的作用域和变量副本 | 步骤 | 发生了什么 | `count` 的值 | |------|-----------|-------------| | **第 1 次渲染** | React 执行函数体,所有变量被"定格"在这个渲染里 | `count = 0` | | 用户点击按钮 | 回调 `handleClick` 被创建并立即执行 | `count` 仍是 `0`(闭包捕获了快照) | | 注册 `setTimeout` | 只是说"1 秒后回来执行这段代码" | — | | 1 秒后回调执行 | 调用 `setCount(0+1)` → React 触发**第 2 次渲染** | — | | **第 2 次渲染** | React 重新执行整个函数体,`count` 更新为 `1` | `count = 1` | ### 为什么不是异步的问题? 这不是因为 `setTimeout` 是异步 API,而是**闭包捕获了渲染时那个时刻的变量值**。 ```js // 换个定时器也一样——问题不在 async,而在 closure Promise.resolve().then(() => setCount(count + 1)); // ❌ 同样捕获 0 // 即使同步也会一样 const addOne = () => count + 1; // 执行时 count 还是 0 setCount(addOne()); // ❌ ``` ### 对比普通变量(非 Hook) ```js let count = 0; // ✅ 同一个变量,可自由修改 setTimeout(() => count++, 1000); // count 正常变成 1 ``` 原因:`let` 声明的是**可变引用**,不存在"快照"概念。而 React Hooks 机制下,每次渲染都会创建一个全新的变量副本,`handleClick` 作为闭包引用的永远是第一次渲染时的那个副本。 ## 正确写法 ```tsx // ✅ 方案 1:updater 函数 —— 让 React 提供最新状态 setTimeout(() => setCount(prev => prev + 1), 1000); // ✅ 方案 2:保存当前快照 const currentCount = count; setTimeout(() => setCount(currentCount + 1), 1000); // ✅ 方案 3:用 useRef 持有一个总是最新的 ref const countRef = useRef(count); countRef.current = count; // 每次渲染都更新 setTimeout(() => setCount(countRef.current + 1), 1000); ``` ## 总结 > [!summary] 一句话记住 > React 的闭包陷阱本质:**闭包捕获的是渲染时刻的变量快照,不会自动追踪后续更新。** > > 解决思路就两条:要么让 React 给最新值(`setCount(prev => ...)`),要么自己存一份实时更新的(`useRef`)。