在 React Hooks 中的 閉包陷阱(Closure Trap)在 useEffect
、事件回調、定時器等場景里很常見。
1. 閉包陷阱是什么
- 當你在函數組件里定義一個回調(比如事件處理函數),這個回調會捕獲當時渲染時的變量值。
- 如果后面狀態更新了,但回調里引用的仍然是舊的變量值(因為它閉包捕獲的是舊值),就會出現狀態不同步的問題。
2. 典型例子
import React, { useState, useEffect } from "react";export default function App() {const [count, setCount] = useState(0);useEffect(() => {const timer = setInterval(() => {console.log(count); // 總是打印 0(閉包陷阱)setCount(count + 1); // 永遠基于舊值}, 1000);return () => clearInterval(timer);}, []); // ? 空依賴數組,count 不會更新return <h1>{count}</h1>;
}
現象:
- 你期望每秒加 1,但實際
count
永遠停在 1 或只打印舊值。 - 原因:
useEffect
只在首次渲染執行一次,所以定時器回調里捕獲的是第一次渲染時的 count。
3. 為什么會發生
- React 函數組件每次渲染都是一個新的執行上下文。
- 變量值是“渲染快照” ,渲染完成后不會自動更新到已創建的閉包中。
- 當回調函數使用了上一次渲染的變量,就會變成“舊值引用”。
4. 常見觸發場景
場景 | 問題原因 |
---|---|
setInterval 、setTimeout | 定時器回調捕獲了舊狀態 |
事件回調 | 綁定時的函數引用了舊值 |
異步請求回調 | then/callback 捕獲了舊狀態 |
WebSocket、監聽器 | 回調綁定后狀態不會自動刷新 |
5. 解決方案
方案 1:依賴數組聲明最新狀態
useEffect(() => {const timer = setInterval(() => {console.log(count);setCount(count + 1);}, 1000);return () => clearInterval(timer);
}, [count]); // ? 每次 count 變化時重新綁定定時器
缺點:可能頻繁解綁/綁定監聽器。
方案 2:使用函數式更新
useEffect(() => {const timer = setInterval(() => {setCount(prev => prev + 1); // ? 始終基于最新值}, 1000);return () => clearInterval(timer);
}, []); // 依賴數組可以為空
優勢:避免閉包陷阱,保持依賴穩定。
方案 3:使用 useRef
存儲最新值
const countRef = useRef(count);
useEffect(() => {countRef.current = count; // ? 每次渲染更新最新值
});useEffect(() => {const timer = setInterval(() => {console.log(countRef.current); // 始終是最新值setCount(c => c + 1);}, 1000);return () => clearInterval(timer);
}, []);
適合定時器、事件監聽等需要穩定回調的場景。
方案 4:使用 useCallback
保證函數引用穩定
const handleClick = useCallback(() => {console.log(count); // count 會更新,因為依賴變了
}, [count]);
不過這會導致依賴變化時重新生成函數引用,適合事件處理但不適合頻繁綁定解綁的監聽。
6. 一句話總結
React Hooks 中的閉包陷阱 = 回調函數捕獲了舊的狀態值,導致邏輯和 UI 不同步。
核心解決思路:要么讓回調用到的狀態實時更新(函數式更新 / ref) ,要么確保回調重新生成(依賴數組) 。