首先先看一段代碼:
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的第二個(gè)參數(shù)為空數(shù)組,所以只會在組件加載后僅執(zhí)行一次,我們知道組件每次render的時(shí)候都會生成一個(gè)新的state對象,對應(yīng)一個(gè)快照,上述代碼中,因?yàn)閡seEffect只執(zhí)行了一次,所以定時(shí)器中的count 一直是最初快照里的count,那么頁面中count的顯示肯定不會改變;
閉包陷阱產(chǎn)生的原因就是 useEffect 的函數(shù)里引用了某個(gè) state,形成了閉包(也有叫過時(shí)的閉包)
那么我們怎么樣才能每次都拿到最新的count呢?
解決一:使用useEffect的第二個(gè)參數(shù),count變化時(shí),重新執(zhí)行setInterval,并且在useEffect的清理函數(shù)中執(zhí)行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;
但是?。?!這種方法有一定的缺點(diǎn),因?yàn)槊看蝐ount變了都要重置定制器,這樣可能會導(dǎo)致計(jì)時(shí)不準(zhǔn)確;
所以,這種把依賴的 state 添加到 deps 里的方式是能解決閉包陷阱,但是定時(shí)器不能這樣做;
我們采用useRef的方式?。?!
解法二:最主要的是setCount(count => count +1),使用函數(shù)作為參數(shù),接受一個(gè)舊的state,得到新的state;
使用useRef來保存回調(diào)函數(shù),在useEffect中從 ref.current 來取函數(shù)再調(diào)用,在useLayoutEffect中給ref賦值新的fn,這個(gè)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ù),接受一個(gè)舊的state,得到新的state
//所以就會render
setCount(count => count + 1);
}, 1000);
}, []);
//每次在render前都給ref賦值新的fn,這個(gè)fn里的state是最新值
useLayoutEffect(() => {
ref.current = fn;
});
useEffect(() => {
setInterval(() => ref.current(), 1000);
}, []);
return <div>count: {count}</div>;
}
export default App;
以上這個(gè)代碼可以封裝成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;
擴(kuò)展知識
- 使用
useEffect時(shí),若有多個(gè)副作用,則應(yīng)該調(diào)用多個(gè)useEffect,而不是寫在一個(gè)里面; -
useEffect第一個(gè)參數(shù)可以返回一個(gè)函數(shù),這個(gè)函數(shù)會在組件卸載時(shí)(也就是render了,生成新的快照時(shí))執(zhí)行,可以用來清除副作用里的操作; -
useLayoutEffect是在render前同步執(zhí)行的(和componentDidMount等價(jià)),useEffect是在render后異步執(zhí)行的;