Files
cs-note/hhs/REACT/2. Hooks 篇/05-核心 Hooks/01-闭包陷阱.md
T
2026-05-24 11:42:38 +08:00

80 lines
2.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 <button onClick={handleClick}>{count}</button>;
}
```
## 结合组件渲染的生命周期拆解
> [!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`)。