useEffect的閉包陷阱及useInterval

首先先看一段代碼:

import { useEffect, useState } from 'react';

const App = () => {
    const [count,setCount] = useState(0);

    useEffect(() => {
        setInterval(() => {
            setCount(count + 1);
        }, 500);
    }, []);

    useEffect(() => {
        setInterval(() => {
            console.log(count);
        }, 500);
    }, []);

    return <div>count: {count}</div>;
}

export default App;

結(jié)果是:頁面上count一直顯示1;
解析:useEffect的第二個(gè)參數(shù)為空數(shù)組,所以只會在組件加載后僅執(zhí)行一次,我們知道組件每次render的時(shí)候都會生成一個(gè)新的state對象,對應(yīng)一個(gè)快照,上述代碼中,因?yàn)閡seEffect只執(zhí)行了一次,所以定時(shí)器中的count 一直是最初快照里的count,那么頁面中count的顯示肯定不會改變;

閉包陷阱產(chǎn)生的原因就是 useEffect 的函數(shù)里引用了某個(gè) state,形成了閉包(也有叫過時(shí)的閉包)

那么我們怎么樣才能每次都拿到最新的count呢?
解決一:使用useEffect的第二個(gè)參數(shù),count變化時(shí),重新執(zhí)行setInterval,并且在useEffect的清理函數(shù)中執(zhí)行clearInterval,這樣我們就可以在頁面上看到變化的count了??!

import { useEffect, useState } from 'react';

const App = () => {
    const [count,setCount] = useState(0);

    useEffect(() => {
       const timer = setInterval(() => {
            setCount(count + 1);
        }, 1000);
      return () => clearInterval(timer)
    }, [count]);

    useEffect(() => {
         const timer = setInterval(() => {
            console.log(count);
        }, 1000);
        return () => clearInterval(timer)
    }, [count]);

    return <div>count: {count}</div>;
}

export default App;

但是?。?!這種方法有一定的缺點(diǎn),因?yàn)槊看蝐ount變了都要重置定制器,這樣可能會導(dǎo)致計(jì)時(shí)不準(zhǔn)確;
所以,這種把依賴的 state 添加到 deps 里的方式是能解決閉包陷阱,但是定時(shí)器不能這樣做;
我們采用useRef的方式?。?!

解法二:最主要的是setCount(count => count +1),使用函數(shù)作為參數(shù),接受一個(gè)舊的state,得到新的state;
使用useRef來保存回調(diào)函數(shù),在useEffect中從 ref.current 來取函數(shù)再調(diào)用,在useLayoutEffect中給ref賦值新的fn,這個(gè)fn里的state是最新的;

import { useEffect, useLayoutEffect, useRef } from 'react';

const App = () => {
    const [count,setCount] = useState(0);

    const fn = () => {
        //還可以做一些其他邏輯操作
        console.log(count);
    };
    
    const ref = useRef(()=>{});

   useEffect(() => {
        setInterval(() => {
            //最關(guān)鍵的一步,使用函數(shù),接受一個(gè)舊的state,得到新的state
            //所以就會render
            setCount(count => count + 1);
        }, 1000);
    }, []);
    
    //每次在render前都給ref賦值新的fn,這個(gè)fn里的state是最新值
    useLayoutEffect(() => {
        ref.current = fn;
    });

    useEffect(() => {
        setInterval(() => ref.current(), 1000);
    }, []);

    return <div>count: {count}</div>;
}

export default App;

以上這個(gè)代碼可以封裝成useInterval

//useInterval
import { useEffect, useLayoutEffect, useRef } from 'react';

const useInterval = (fn: Function, delay: number)=>{
    const ref = useRef<Function>(()=>{})

    useLayoutEffect(()=>{
        ref.current = fn
    })

    useEffect(()=>{
        setInterval(()=>{
            ref.current()
        }, delay)
    }, [])
}

export default useInterval
import useInterval from './useInterval';

const App = () => {
    const [count,setCount] = useState(0);
    useInterval(()=>{
        setCount(count => count+1)
    }, 1000)
    useInterval(()=>{
        console.log(count, 'count')
    }, 1000)
    
    return <div>count: {count}</div>;
}

export default App;

擴(kuò)展知識

  • 使用useEffect時(shí),若有多個(gè)副作用,則應(yīng)該調(diào)用多個(gè)useEffect,而不是寫在一個(gè)里面;
  • useEffect第一個(gè)參數(shù)可以返回一個(gè)函數(shù),這個(gè)函數(shù)會在組件卸載時(shí)(也就是render了,生成新的快照時(shí))執(zhí)行,可以用來清除副作用里的操作;
  • useLayoutEffect是在render前同步執(zhí)行的(和componentDidMount等價(jià)),useEffect是在render后異步執(zhí)行的;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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