美文网首页
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