useEffect 是 React 提供的用于處理副作用的 Hook(副作用:數(shù)據(jù)請求、DOM 操作、訂閱 / 取消訂閱、定時(shí)器 / 延時(shí)器等)。useEffect 接收兩個(gè)參數(shù):
- 第一個(gè)參數(shù):副作用函數(shù)(必選),可以返回一個(gè)清理函數(shù)(可選);
- 第二個(gè)參數(shù):依賴數(shù)組(可選),存放副作用函數(shù)中用到的所有響應(yīng)式變量(state、props、組件內(nèi)定義的函數(shù) / 變量)。
1.初級(jí) useEffect怎么用?
- 組件掛載后執(zhí)行(依賴數(shù)組為空
[]);componentDidMount - 依賴項(xiàng)變化時(shí)執(zhí)行(依賴數(shù)組填指定變量);
componentDidUpdate - 組件卸載時(shí)清理(返回清理函數(shù));
componentWillUnmount - 每次渲染后執(zhí)行(如果不寫第二個(gè)參數(shù))。
2. 進(jìn)階-使用 useEffect 時(shí)遇到的坑點(diǎn)及解決方法
坑點(diǎn)1:依賴數(shù)組漏寫 / 錯(cuò)寫
- 坑:副作用函數(shù)中用到了某個(gè)變量,但沒寫進(jìn)依賴數(shù)組 → React 無法監(jiān)控該變量變化,副作用不會(huì)重新執(zhí)行;
const [count, setCount] = useState(0);
// 坑:用到了count,但依賴數(shù)組為空 → 點(diǎn)擊按鈕count變化,副作用不會(huì)重新執(zhí)行
useEffect(() => {
console.log(count);
}, []);
- 解決:嚴(yán)格遵循「依賴數(shù)組包含副作用內(nèi)所有用到的響應(yīng)式變量」,可開啟 ESLint 規(guī)則(
react-hooks/exhaustive-deps)自動(dòng)檢查。
坑點(diǎn)2: 依賴數(shù)組放引用類型(對象 / 數(shù)組 / 函數(shù))
- 坑:React對對象實(shí)例進(jìn)行監(jiān)控,引用類型每次渲染會(huì)生成新引用 → 即使內(nèi)容沒變,也會(huì)觸發(fā)副作用重復(fù)執(zhí)行;
const [data, setData] = useState({ name: 'test' ,id:1});
// 坑:data是對象,每次渲染都是新引用 → 副作用每次都執(zhí)行
useEffect(() => {
console.log(data);
}, [data]);
- 解決對對象的某個(gè)屬性進(jìn)行監(jiān)控,只有改屬性發(fā)生變化,才會(huì)執(zhí)行副作用
useEffect(() => {
console.log(data);
}, [data.name]);
- 函數(shù)依賴:用 useCallback 緩存函數(shù)
useCallback 會(huì)緩存函數(shù)的引用,只有當(dāng)依賴項(xiàng)變化時(shí),才會(huì)生成新函數(shù),避免因引用變化觸發(fā)副作用。
解決方案代碼:
- 函數(shù)依賴:用 useCallback 緩存函數(shù)
import { useState, useEffect, useCallback } from 'react';
function Demo() {
const [data, setData] = useState({ name: 'test' });
// 用 useCallback 緩存函數(shù):依賴 data.name(僅該屬性變化時(shí),函數(shù)引用才變)
const handlePrintData = useCallback(() => {
console.log('當(dāng)前 data:', data.name); // 函數(shù)內(nèi)用到 data.name,需加入依賴
}, [data.name]); // 關(guān)鍵:依賴函數(shù)內(nèi)用到的響應(yīng)式數(shù)據(jù)(data.name)
// 依賴緩存后的函數(shù) → 僅 handlePrintData 引用變化(即 data.name 變化)時(shí)執(zhí)行
useEffect(() => {
console.log('副作用執(zhí)行(函數(shù)依賴-已緩存)');
handlePrintData();
}, [handlePrintData]);
...
}
...
}
- 對象依賴:用
useMemo緩存,或只依賴具體屬性(如data.name)用 useMemo 緩存對象(推薦,適合需完整對象)useMemo 緩存對象引用,只有依賴項(xiàng)變化時(shí)才生成新對象。
- 對象依賴:用
import { useState, useEffect, useMemo } from 'react';
function Demo() {
const [data, setData] = useState({ name: 'test' });
// 用useMemo緩存對象,依賴age(只有age變化時(shí),對象引用才變)
const user = useMemo(() => {
return { name: data.name, age: 18 }; // 對象內(nèi)用到的變量加入依賴
}, [data.name]);
// 依賴緩存后的對象 → 只有user引用變化(即age變化)時(shí)才執(zhí)行
useEffect(() => {
console.log('副作用執(zhí)行(對象依賴-已緩存)', user);
}, [user]);
...
}
坑點(diǎn) 3:閉包陷阱(拿不到最新的 state/props)
- 現(xiàn)象:副作用函數(shù)中拿到的變量永遠(yuǎn)是初始值,即使變量已更新;
- 原因:
useEffect的副作用函數(shù)捕獲了組件渲染時(shí)的變量,若依賴數(shù)組未包含該變量,副作用不會(huì)重新執(zhí)行,始終使用舊值;
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
// 永遠(yuǎn)輸出0,因?yàn)橐蕾嚁?shù)組為空,捕獲的是初始count
console.log(count);
}, 1000);
}, []);
- 解決:把變量加入依賴數(shù)組(推薦):
useEffect(() => {
const timer = setInterval(() => console.log(count), 1000);
return () => clearInterval(timer);
}, [count]); // 依賴count,每次count變化重新創(chuàng)建定時(shí)器
- 若不想頻繁創(chuàng)建定時(shí)器,用
useRef保存最新值:
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // 每次count更新,同步到ref
}, [count]);
useEffect(() => {
setInterval(() => {
console.log(countRef.current); // 拿到最新值
}, 1000);
}, []);
坑點(diǎn) 2:清理函數(shù)執(zhí)行時(shí)機(jī)理解錯(cuò)誤
- 現(xiàn)象:認(rèn)為清理函數(shù)只在組件卸載時(shí)執(zhí)行,忽略「依賴變化時(shí)也會(huì)執(zhí)行」;
- 原因:清理函數(shù)的執(zhí)行時(shí)機(jī)是「下一次副作用執(zhí)行前 + 組件卸載時(shí)」;明確清理函數(shù)的作用:不僅是卸載清理,也是「副作用更新前的收尾」;清理邏輯要和副作用邏輯對應(yīng)(如創(chuàng)建定時(shí)器→清除定時(shí)器,發(fā)起請求→標(biāo)記取消)。
const [id, setId] = useState(1);
useEffect(() => {
console.log(`請求id: ${id}`);
const timer = setTimeout(() => {}, 1000);
return () => {
console.log(`清理id: ${id}`); // id變化時(shí)會(huì)執(zhí)行,不是只有卸載時(shí)
clearTimeout(timer);
};
}, [id]);
坑點(diǎn) 3:在 useEffect 中直接修改 state 導(dǎo)致無限循環(huán)
- 現(xiàn)象:副作用函數(shù)中修改 state,且依賴數(shù)組包含該 state → 無限觸發(fā)更新;
- 示例:
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 修改count,依賴數(shù)組包含count → 無限循環(huán)
}, [count]);
- 解決:若修改 state 不需要依賴舊值,用函數(shù)式更新:
useEffect(() => {
// 不依賴外部count,依賴數(shù)組為空
setCount(prev => prev + 1);
}, []);
- 若必須依賴,加條件判斷限制執(zhí)行:
useEffect(() => {
if (count < 10) {
setCount(count + 1);
}
}, [count]);
坑點(diǎn) 4:忽略 useEffect 的異步執(zhí)行特性
- 現(xiàn)象:在 useEffect 中獲取 DOM 元素,卻想在組件渲染前拿到;
- 原因:
useEffect是異步執(zhí)行的,在 DOM 繪制后才執(zhí)行,而useLayoutEffect是同步執(zhí)行(DOM 繪制前); - 解決:
- 普通 DOM 操作:用
useEffect即可(DOM 已掛載); - 需同步操作 DOM(如測量尺寸、避免頁面閃爍):用
useLayoutEffect。
- 普通 DOM 操作:用
總結(jié)
初級(jí)核心:掌握
useEffect的基本用法、執(zhí)行時(shí)機(jī)、依賴數(shù)組規(guī)則,避免漏寫 / 錯(cuò)寫依賴,正確處理耗時(shí)操作;進(jìn)階核心:理解
useEffect的底層機(jī)制(Hook 鏈表、淺比較、執(zhí)行階段),解決閉包、重復(fù)執(zhí)行、競態(tài)等實(shí)戰(zhàn)問題;關(guān)鍵原則:「依賴數(shù)組必須包含副作用內(nèi)所有用到的響應(yīng)式變量」+「清理函數(shù)與副作用邏輯對應(yīng)」
3. 高手- useEffect的設(shè)計(jì)原理?(待補(bǔ)充)
為什么useEffect可以對屬性進(jìn)行監(jiān)控 (待補(bǔ)充)
useEffect會(huì)被多次調(diào)用么?如何解決useEffect重復(fù)調(diào)用(待補(bǔ)充)