陷阱題:閉包問題、Stale Closure舉例
一、依賴項為空數組[]
與不寫的核心區別
行為 | 空數組[] | 不寫依賴項 |
---|---|---|
執行時機 | 僅在組件掛載時執行一次(類似componentDidMount ) | 組件每次渲染后都執行(類似componentDidUpdate ) |
更新觸發條件 | 永不觸發(除非組件卸載后重新掛載) | 任何狀態或屬性變化都會觸發 |
清理函數執行時機 | 僅在組件卸載時執行一次 | 每次重新渲染前都會執行清理函數 |
二、閉包陷阱(Stale Closure)示例
案例1:定時器中的舊值引用
function Counter() {const [count, setCount] = useState(0);useEffect(() => {const timer = setInterval(() => {// 閉包陷阱:始終引用初始值0console.log(count); }, 1000);return () => clearInterval(timer);}, []); // 依賴項為空數組return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
? 問題:定時器回調函數中的count
始終是初始值0
,因為閉包捕獲了初始渲染時的狀態。
? 原因:空數組依賴項導致useEffect
僅執行一次,內部閉包未更新。
案例2:依賴項缺失導致無限循環
useEffect(() => {setCount(count + 1); // 未設置依賴項,每次渲染觸發更新
});
? 結果:組件陷入無限渲染循環。
三、閉包問題的解決方案
方案1:正確設置依賴項
useEffect(() => {const timer = setInterval(() => {console.log(count); // 每次count變化時獲取最新值}, 1000);return () => clearInterval(timer);
}, [count]); // 依賴項包含count
? 原理:依賴項變化時,重新生成閉包函數。
方案2:使用useRef
繞過閉包
const countRef = useRef(count);
useEffect(() => {countRef.current = count; // 手動同步最新值到ref
});useEffect(() => {const timer = setInterval(() => {console.log(countRef.current); // 通過ref獲取最新值}, 1000);return () => clearInterval(timer);
}, []);
? 優勢:避免依賴項導致定時器頻繁重置。
方案3:函數式更新狀態
useEffect(() => {const timer = setInterval(() => {setCount(c => c + 1); // 函數式更新,避免依賴舊值}, 1000);return () => clearInterval(timer);
}, []);
? 原理:通過函數參數獲取最新狀態值,無需依賴項。
四、最佳實踐與性能優化
- 依賴項規則:確保所有引用的外部變量(包括
props
和state
)都出現在依賴數組中。 - 避免對象/數組依賴:直接傳遞對象屬性或使用
useMemo
優化引用類型。 - 并發模式兼容:React 18中,
useEffect
可能被中斷渲染,優先使用useLayoutEffect
處理DOM同步操作。
五、總結對比表
場景 | 空數組[] | 無依賴項 |
---|---|---|
典型用途 | 初始化請求、一次性訂閱 | 響應式DOM操作、全局事件監聽 |
閉包風險 | 高(依賴舊值) | 低(每次更新生成新閉包) |
性能影響 | 低(僅執行一次) | 高(頻繁觸發) |
通過合理選擇依賴項策略,可顯著提升組件性能和邏輯健壯性。