首先先看一段代码:
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后异步执行的;
网友评论