首先先看一段代碼:
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的第二個參數(shù)為空數(shù)組牵啦,所以只會在組件加載后僅執(zhí)行一次迫皱,我們知道組件每次render的時候都會生成一個新的state對象茎活,對應(yīng)一個快照昙沦,上述代碼中,因為useEffect只執(zhí)行了一次载荔,所以定時器中的count
一直是最初快照里的count
桅滋,那么頁面中count
的顯示肯定不會改變;
閉包陷阱產(chǎn)生的原因就是 useEffect 的函數(shù)里引用了某個 state身辨,形成了閉包(也有叫過時的閉包)
那么我們怎么樣才能每次都拿到最新的count
呢丐谋?
解決一:使用useEffect的第二個參數(shù),count變化時煌珊,重新執(zhí)行setInterval
号俐,并且在useEffect的清理函數(shù)中執(zhí)行clearInterval
,這樣我們就可以在頁面上看到變化的count了6ㄢ帧吏饿!
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;
但是!J哒恪猪落!這種方法有一定的缺點,因為每次count變了都要重置定制器畴博,這樣可能會導(dǎo)致計時不準(zhǔn)確笨忌;
所以,這種把依賴的 state 添加到 deps 里的方式是能解決閉包陷阱俱病,但是定時器不能這樣做官疲;
我們采用useRef
的方式8そ帷!途凫!
解法二:最主要的是setCount(count => count +1)
垢夹,使用函數(shù)作為參數(shù),接受一個舊的state维费,得到新的state果元;
使用useRef
來保存回調(diào)函數(shù),在useEffect
中從 ref.current
來取函數(shù)再調(diào)用犀盟,在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(() => {
//最關(guān)鍵的一步且蓬,使用函數(shù)欣硼,接受一個舊的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
時,若有多個副作用豹障,則應(yīng)該調(diào)用多個useEffect
冯事,而不是寫在一個里面; -
useEffect
第一個參數(shù)可以返回一個函數(shù)血公,這個函數(shù)會在組件卸載時(也就是render了昵仅,生成新的快照時)執(zhí)行,可以用來清除副作用里的操作累魔; -
useLayoutEffect
是在render前同步執(zhí)行的(和componentDidMount
等價)摔笤,useEffect
是在render后異步執(zhí)行的;