一、React 閉包陷阱詳解
1. 什么是閉包陷阱
React 閉包陷阱是指在函數組件中使用 Hook(特別是?useEffect
?和?useCallback
)時,由于閉包特性導致訪問到舊的 state 或 props 值,而非最新值的現象。
2. 典型場景示例
function Counter() {const [count, setCount] = useState(0);useEffect(() => {const timer = setInterval(() => {console.log(count); // 這里總是打印初始值0setCount(count + 1);}, 1000);return () => clearInterval(timer);}, []); // 空依賴數組return <div>{count}</div>;
}
3. 產生原因分析
閉包特性:Effect 回調函數捕獲了初始渲染時的?
count
?值依賴數組:空數組意味著 Effect 只在掛載時運行一次
每次渲染獨立:每次渲染都有獨立的 props/state 和 Effect
二、解決閉包陷阱的 5 種方法
1. 正確聲明依賴項
useEffect(() => {const timer = setInterval(() => {setCount(count + 1);}, 1000);return () => clearInterval(timer);
}, [count]); // 添加count依賴
2. 使用函數式更新
useEffect(() => {const timer = setInterval(() => {setCount(prev => prev + 1); // 使用前一個值}, 1000);return () => clearInterval(timer);
}, []); // 不需要添加count依賴
3. 使用 useRef 保存可變值
function Counter() {const [count, setCount] = useState(0);const countRef = useRef(count);useEffect(() => {countRef.current = count; // 每次渲染更新ref});useEffect(() => {const timer = setInterval(() => {console.log(countRef.current); // 訪問最新值setCount(countRef.current + 1);}, 1000);return () => clearInterval(timer);}, []);return <div>{count}</div>;
}
4. 使用 useReducer
function reducer(state, action) {if (action.type === 'tick') {return state + 1;}throw new Error();
}function Counter() {const [state, dispatch] = useReducer(reducer, 0);useEffect(() => {const timer = setInterval(() => {dispatch({ type: 'tick' });}, 1000);return () => clearInterval(timer);}, []); // 不需要依賴return <div>{state}</div>;
}
5. 自定義 Hook 封裝
function useInterval(callback, delay) {const savedCallback = useRef();useEffect(() => {savedCallback.current = callback;});useEffect(() => {function tick() {savedCallback.current();}if (delay !== null) {const id = setInterval(tick, delay);return () => clearInterval(id);}}, [delay]);
}// 使用
function Counter() {const [count, setCount] = useState(0);useInterval(() => {setCount(count + 1);}, 1000);return <div>{count}</div>;
}
三、React 16/17/18 主要區別
React 16.x 主要特性
Fiber 架構(16.0)
新的協調算法
支持異步渲染(但默認未啟用)
Hooks(16.8)
useState, useEffect 等 Hook API
函數組件能力大幅增強
Error Boundaries(16.0)
componentDidCatch 生命周期
更好的錯誤處理機制
Context API(16.3)
新的 Context API 設計
替代舊版 context
React 17.x 主要特性
事件委托變更(17.0)
事件不再附加到 document,而是附加到 root DOM
解決多版本 React 共存問題
漸進式升級(17.0)
更容易逐步升級 React 版本
為 React 18 做準備
新的 JSX 轉換(17.0)
無需引入 React 即可使用 JSX
自動從 react/jsx-runtime 導入
Effect 清理時機(17.0)
useEffect 清理函數改為異步執行
與 componentWillUnmount 行為一致
React 18.x 主要特性
并發渲染(18.0)
新的 createRoot API
startTransition, useTransition
useDeferredValue
自動批處理(18.0)
自動合并多個狀態更新
包括 Promise、setTimeout 等
新的 Hook(18.0)
useId:生成唯一 ID
useSyncExternalStore:外部存儲集成
useInsertionEffect:CSS-in-JS 庫使用
流式 SSR(18.0)
Suspense 支持服務端渲染
選擇性注水(Selective Hydration)
四、版本升級對比表
特性 | React 16 | React 17 | React 18 |
---|---|---|---|
架構 | Fiber | Fiber | 并發Fiber |
Hook支持 | 16.8+ | 是 | 是 |
事件系統 | document | root | root |
批處理 | 僅React事件 | 僅React事件 | 全自動 |
SSR | 傳統 | 傳統 | 流式+選擇性注水 |
新API | Context, Error Boundaries | 無 | Transition, Suspense等 |
JSX轉換 | 經典 | 新/經典 | 新 |
默認渲染模式 | 同步 | 同步 | 并發可選 |
五、實際開發建議
閉包陷阱防范:
始終檢查 Hook 依賴數組
優先使用函數式更新
復雜場景使用 useReducer
版本選擇建議:
新項目直接使用 React 18
現有項目逐步升級到 18(通過 17 過渡)
需要 IE11 支持的項目可停留在 17
升級注意事項:
# 從16/17升級到18的步驟 npm install react@18 react-dom@18 # 修改入口文件 import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById('root')); root.render(<App />);
并發特性采用策略:
// 逐步采用并發特性 function App() {const [isPending, startTransition] = useTransition();const handleClick = () => {startTransition(() => {// 非緊急狀態更新setResource(fetchData());});};return (<Suspense fallback={<Spinner />}><Component /></Suspense>); }
理解 React 閉包陷阱和版本差異有助于編寫更健壯的 React 應用,并合理規劃項目升級路線。