vault backup: 2026-04-30 11:24:07
This commit is contained in:
@@ -78,6 +78,20 @@ const [state, setState] = useState<T>(initialValue);
|
|||||||
const [items, setItems] = useState(() => loadFromStorage()); // 仅首次渲染执行
|
const [items, setItems] = useState(() => loadFromStorage()); // 仅首次渲染执行
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 闭包陷阱(Closure Trap)
|
||||||
|
|
||||||
|
> [!warning] 高频踩坑点:React 每次渲染都创建全新的作用域和变量副本
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
setTimeout(() => setCount(count + 1), 1000); // ❌ count 始终为首次渲染时的快照值
|
||||||
|
```
|
||||||
|
|
||||||
|
**为什么?** React 的 `setCount` 不是异步函数,而是 **闭包捕获了渲染时的变量快照** —— 第一次渲染后,`handleClick` 里的 `count` 就被锁死在 `0` 上了。
|
||||||
|
|
||||||
|
> [!tip] 如何修复?
|
||||||
|
> 有两种思路:让 React 自己提供最新状态(updater),或者用 ref 持有一个始终更新的引用。具体方案与对比见 → [[05-核心 Hooks/01-闭包陷阱|详解]]
|
||||||
|
|
||||||
### 关键行为
|
### 关键行为
|
||||||
|
|
||||||
> [!tip] 核心认知
|
> [!tip] 核心认知
|
||||||
@@ -89,22 +103,6 @@ const [items, setItems] = useState(() => loadFromStorage()); // 仅首次渲染
|
|||||||
| 连续多次 setState | React 18 自动合并为一次 re-render |
|
| 连续多次 setState | React 18 自动合并为一次 re-render |
|
||||||
| 在非 React 代码中调用 | 使用 `startTransition` / `flushSync` 控制 |
|
| 在非 React 代码中调用 | 使用 `startTransition` / `flushSync` 控制 |
|
||||||
|
|
||||||
```tsx
|
|
||||||
function BadCounter() {
|
|
||||||
const [count, setCount] = useState(0);
|
|
||||||
|
|
||||||
// ❌ 闭包陷阱:count 始终捕获定义时的 0
|
|
||||||
setTimeout(() => setCount(count + 1), 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function GoodCounter() {
|
|
||||||
const [count, setCount] = useState(0);
|
|
||||||
|
|
||||||
// ✅ 使用 updater 函数,获取最新值
|
|
||||||
setTimeout(() => setCount(c => c + 1), 1000);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 不可变更新模式
|
### 不可变更新模式
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
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`)。
|
||||||
Reference in New Issue
Block a user