From f8c6afa9bd4ce4d3ed566a075a8a541badb4198d Mon Sep 17 00:00:00 2001 From: wonder Date: Thu, 30 Apr 2026 11:24:07 +0800 Subject: [PATCH] vault backup: 2026-04-30 11:24:07 --- hhs/REACT/2. Hooks 篇/05-核心 Hooks.md | 30 ++++--- .../2. Hooks 篇/05-核心 Hooks/01-闭包陷阱.md | 79 +++++++++++++++++++ 2 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 hhs/REACT/2. Hooks 篇/05-核心 Hooks/01-闭包陷阱.md diff --git a/hhs/REACT/2. Hooks 篇/05-核心 Hooks.md b/hhs/REACT/2. Hooks 篇/05-核心 Hooks.md index 71ef945..4b5ee33 100644 --- a/hhs/REACT/2. Hooks 篇/05-核心 Hooks.md +++ b/hhs/REACT/2. Hooks 篇/05-核心 Hooks.md @@ -78,6 +78,20 @@ const [state, setState] = useState(initialValue); 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] 核心认知 @@ -89,22 +103,6 @@ const [items, setItems] = useState(() => loadFromStorage()); // 仅首次渲染 | 连续多次 setState | React 18 自动合并为一次 re-render | | 在非 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 diff --git a/hhs/REACT/2. Hooks 篇/05-核心 Hooks/01-闭包陷阱.md b/hhs/REACT/2. Hooks 篇/05-核心 Hooks/01-闭包陷阱.md new file mode 100644 index 0000000..025968c --- /dev/null +++ b/hhs/REACT/2. Hooks 篇/05-核心 Hooks/01-闭包陷阱.md @@ -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 ; +} +``` + +## 结合组件渲染的生命周期拆解 + +> [!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`)。