美文网首页
useEffect的闭包陷阱及useInterval

useEffect的闭包陷阱及useInterval

作者: darkTi | 来源:发表于2022-05-17 00:32 被阅读0次

    首先先看一段代码:

    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;
    

    扩展知识

    • 使用useEffect时,若有多个副作用,则应该调用多个useEffect,而不是写在一个里面;
    • useEffect第一个参数可以返回一个函数,这个函数会在组件卸载时(也就是render了,生成新的快照时)执行,可以用来清除副作用里的操作;
    • useLayoutEffect是在render前同步执行的(和componentDidMount等价),useEffect是在render后异步执行的;

    相关文章

      网友评论

          本文标题:useEffect的闭包陷阱及useInterval

          本文链接:https://www.haomeiwen.com/subject/ydpwurtx.html