React面試必問-useEffect小結(jié),由淺到深

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]);
    1. 解決對對象的某個(gè)屬性進(jìn)行監(jiān)控,只有改屬性發(fā)生變化,才會(huì)執(zhí)行副作用
useEffect(() => {

 console.log(data);

}, [data.name]);
    1. 函數(shù)依賴:用 useCallback 緩存函數(shù)
      useCallback 會(huì)緩存函數(shù)的引用,只有當(dāng)依賴項(xiàng)變化時(shí),才會(huì)生成新函數(shù),避免因引用變化觸發(fā)副作用。
      解決方案代碼:
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]);
  ...
}
 ...
}
    1. 對象依賴:用 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。

總結(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ǔ)充)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容