從根上理解 React Hooks 的閉包陷阱(續(xù)集)
??上篇文章??我們知道了什么是 hooks 的閉包陷阱,它的產(chǎn)生原因和解決方式,并通過一個(gè)案例做了演示。
其實(shí)那個(gè)案例的閉包陷阱的解決方式不夠完善,這篇文章我們?cè)偻晟埔幌隆?/p>
首先我們先來回顧下什么是閉包陷阱:
hooks 的閉包陷阱是指 useEffect 等 hook 中用到了某個(gè) state,但是沒有把它加到 deps 數(shù)組里,導(dǎo)致 state 變了,但是執(zhí)行的函數(shù)依然引用著之前的 state。
它的解決方式就是正確設(shè)置 deps 數(shù)組,把用到的 state 放到 deps 數(shù)組里,這樣每次 state 變了就能執(zhí)行最新的函數(shù),引用新的 state。同時(shí)要清理上次的定時(shí)器、事件監(jiān)聽器等。
我們舉了這樣一個(gè)例子:
import { useEffect, useState } from 'react';
function Dong() {
const [count,setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 500);
}, []);
useEffect(() => {
setInterval(() => {
console.log(count);
}, 500);
}, []);
return <div>guang</div>;
}
export default Dong;
每次打印都是 0 :
解決方式就是把 count 設(shè)置到 deps 里,并添加清理函數(shù):
import { useEffect, useState } from 'react';
function Dong() {
const [count,setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 500);
return () => clearInterval(timer);
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 500);
return () => clearInterval(timer);
}, [count]);
return <div>guang</div>;
}
export default Dong;
這樣就能解決閉包陷阱:
但是這種解決閉包陷阱的方式用在定時(shí)器上不是很合適。
為什么呢?
因?yàn)楝F(xiàn)在每次 count 變了就會(huì)重置定時(shí)器,那之前的計(jì)時(shí)就重新計(jì)算,這樣就會(huì)導(dǎo)致計(jì)時(shí)不準(zhǔn)。
所以,這種把依賴的 state 添加到 deps 里的方式是能解決閉包陷阱,但是定時(shí)器不能這樣做。
那還有什么方式能解決閉包陷阱呢?
useRef。
閉包陷阱產(chǎn)生的原因就是 useEffect 的函數(shù)里引用了某個(gè) state,形成了閉包,那不直接引用不就行了?
useRef 是在 memorizedState 鏈表中放一個(gè)對(duì)象,current 保存某個(gè)值。
它的源碼是這樣的:
初始化的時(shí)候創(chuàng)建了一個(gè)對(duì)象放在 memorizedState 上,后面始終返回這個(gè)對(duì)象。
這樣通過 useRef 保存回調(diào)函數(shù),然后在 useEffect 里從 ref.current 來取函數(shù)再調(diào)用,避免了直接調(diào)用,也就沒有閉包陷阱的問題了。
也就是這樣:
const fn = () => {
console.log(count);
};
const ref = useRef(fn);
useLayoutEffect(() => {
ref.current = fn;
});
useEffect(() => {
setInterval(() => ref.current(), 500);
}, []);
useEffect 里執(zhí)行定時(shí)器,deps 設(shè)置為了 [],所以只會(huì)執(zhí)行一次,回調(diào)函數(shù)用的是 ref.current,沒有直接依賴某個(gè) state,所以不會(huì)有閉包陷阱。
用 useRef 創(chuàng)建個(gè) ref 對(duì)象,初始值為打印 count 的回調(diào)函數(shù),每次 render 都修改下其中的函數(shù)為新創(chuàng)建的函數(shù),這個(gè)函數(shù)里引用的 count 就是最新的。
這里用了 useLayoutEffect 而不是 useEffect 是因?yàn)?useLayoutEffect 是在 render 前同步執(zhí)行的,useEffect 是在 render 后異步執(zhí)行的,所以用 useLayoutEffect 能保證在 useEffect 之前被調(diào)用。
這種方式避免了 useEffect 里直接對(duì) state 的引用,從而避免了閉包問題。
另外,修改 count 的地方,可以用 setCount(count => count + 1) 代替 setCount(count + 1),這樣也就避免了閉包問題:
useEffect(() => {
setInterval(() => {
setCount(count => count + 1);
}, 500);
}, []);
現(xiàn)在組件的代碼是這樣的:
import { useEffect, useLayoutEffect, useState, useRef } from 'react';
function Dong() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count => count + 1);
}, 500);
}, []);
const fn = () => {
console.log(count);
};
const ref = useRef(fn);
useLayoutEffect(() => {
ref.current = fn;
});
useEffect(() => {
setInterval(() => ref.current(), 500);
}, []);
return <div>guang</div>;
}
export default Dong;
測(cè)試下:
確實(shí),打印也是正常的,這就是解決閉包陷阱的第二種方式,通過 useRef 避免直接對(duì) state 的引用,從而避免閉包問題。
這段邏輯用到了多個(gè) hook,可以封裝成個(gè)自定義 hook:
function useInterval(fn, time) {
const ref = useRef(fn);
useLayoutEffect(() => {
ref.current = fn;
});
useEffect(() => {
setInterval(() => ref.current(), time);
}, []);
}
然后組件代碼就可以簡(jiǎn)化了:
function Dong() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 500);
useInterval(() => {
console.log(count);
}, 500);
return <div>guang</div>;
}
這樣我們就用 useRef 的方式解決了閉包陷阱問題。
總結(jié)
上篇文章我們通過把依賴的 state 添加到 deps 數(shù)組中的方式,使得每次 state 變了就執(zhí)行新的函數(shù),引用新的 state,從而解決了閉包陷阱問題。
這種方式用在定時(shí)器上是不合適的,因?yàn)槎〞r(shí)器一旦被重置和重新計(jì)時(shí),那計(jì)時(shí)就不準(zhǔn)確了。
所以我們才用了避免閉包陷阱的第二種方式:使用 useRef。
useRef 能解決閉包陷阱的原因是 useEffect 等 hook 里不直接引用 state,而是引用 ref.current,這樣后面只要修改了 ref 中的值,這里取出來的就是最新的。
然后我們把這段邏輯封裝成了個(gè)自定義 hook,這樣可以方便復(fù)用。
解決 hooks 的閉包陷阱有兩種方式:
- 設(shè)置依賴的 state 到 deps 數(shù)組中并添加清理函數(shù)。
- 不直接引用 state,把 state 放到 useRef 創(chuàng)建的 ref 對(duì)象中再引用。
處理定時(shí)器的時(shí)候,為保證計(jì)時(shí)的準(zhǔn)確,最好使用 useRef 的方式,其余情況兩種都可以。