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

2.7 KiB
Raw Blame History

tags, create time
tags create time
React
Hooks
useState
Closure
Frontend
2026-04-30

闭包陷阱(Closure Trap)

现象

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,而是闭包捕获了渲染时那个时刻的变量值。

// 换个定时器也一样——问题不在 async,而在 closure
Promise.resolve().then(() => setCount(count + 1));  // ❌ 同样捕获 0

// 即使同步也会一样
const addOne = () => count + 1;    // 执行时 count 还是 0
setCount(addOne());                // ❌

对比普通变量(非 Hook)

let count = 0;                        // ✅ 同一个变量,可自由修改
setTimeout(() => count++, 1000);      // count 正常变成 1

原因:let 声明的是可变引用,不存在"快照"概念。而 React Hooks 机制下,每次渲染都会创建一个全新的变量副本,handleClick 作为闭包引用的永远是第一次渲染时的那个副本。

正确写法

// ✅ 方案 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)。