useEffect的闭包陷阱及useInterval

2022-05-17  本文已影响0人  darkTi

首先先看一段代码:

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;

结果是:页面上count一直显示1;
解析:useEffect的第二个参数为空数组,所以只会在组件加载后仅执行一次,我们知道组件每次render的时候都会生成一个新的state对象,对应一个快照,上述代码中,因为useEffect只执行了一次,所以定时器中的count 一直是最初快照里的count,那么页面中count的显示肯定不会改变;

闭包陷阱产生的原因就是 useEffect 的函数里引用了某个 state,形成了闭包(也有叫过时的闭包)

那么我们怎么样才能每次都拿到最新的count呢?
解决一:使用useEffect的第二个参数,count变化时,重新执行setInterval,并且在useEffect的清理函数中执行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;

但是!!!这种方法有一定的缺点,因为每次count变了都要重置定制器,这样可能会导致计时不准确;
所以,这种把依赖的 state 添加到 deps 里的方式是能解决闭包陷阱,但是定时器不能这样做;
我们采用useRef的方式!!!

解法二:最主要的是setCount(count => count +1),使用函数作为参数,接受一个旧的state,得到新的state;
使用useRef来保存回调函数,在useEffect中从 ref.current 来取函数再调用,在useLayoutEffect中给ref赋值新的fn,这个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(() => {
            //最关键的一步,使用函数,接受一个旧的state,得到新的state
            //所以就会render
            setCount(count => count + 1);
        }, 1000);
    }, []);
    
    //每次在render前都给ref赋值新的fn,这个fn里的state是最新值
    useLayoutEffect(() => {
        ref.current = fn;
    });

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

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

export default App;

以上这个代码可以封装成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;

扩展知识

上一篇 下一篇

猜你喜欢

热点阅读