Ahooks 是怎么解決 React 的閉包問(wèn)題的?
本文來(lái)探索一下 ahooks 是怎么解決 React 的閉包問(wèn)題的?
React 的閉包問(wèn)題
先來(lái)看一個(gè)例子:
import React, "react";useState useEffect from
export default () =>
const count setCount = useState(0);
useEffect(() =>
setInterval(() =>
console.log("setInterval:" count);
1000);
);
return (
<div>
count count
<br />
<button onClick= () => setCount((val) => val + 1) >增加 1</button>
</div>
);
;
代碼示例[4]
當(dāng)我點(diǎn)擊按鈕的時(shí)候,發(fā)現(xiàn) setInterval 中打印出來(lái)的值并沒(méi)有發(fā)生變化,始終都是 0。這就是 React 的閉包問(wèn)題。
產(chǎn)生的原因
為了維護(hù) Function Component 的 state,React 用鏈表的方式來(lái)存儲(chǔ) Function Component 里面的 hooks,并為每一個(gè) hooks 創(chuàng)建了一個(gè)對(duì)象。
constcount setCount = useState(0);
useEffect(() =>
setInterval(() =>
console.log("setInterval:" count);
1000);
, );
這個(gè)對(duì)象的 memoizedState 屬性就是用來(lái)存儲(chǔ)組件上一次更新后的 state,next 指向下一個(gè) hook 對(duì)象。在組件更新的過(guò)程中,hooks 函數(shù)執(zhí)行的順序是不變的,就可以根據(jù)這個(gè)鏈表拿到當(dāng)前 hooks 對(duì)應(yīng)的 Hook 對(duì)象,函數(shù)式組件就是這樣擁有了state的能力。
同時(shí)制定了一系列的規(guī)則,比如不能將 hooks 寫(xiě)入到 if...else... 中。從而保證能夠正確拿到相應(yīng) hook 的 state。
useEffect 接收了兩個(gè)參數(shù),一個(gè)回調(diào)函數(shù)和一個(gè)數(shù)組。數(shù)組里面就是 useEffect 的依賴,當(dāng)為 [] 的時(shí)候,回調(diào)函數(shù)只會(huì)在組件第一次渲染的時(shí)候執(zhí)行一次。如果有依賴其他項(xiàng),react 會(huì)判斷其依賴是否改變,如果改變了就會(huì)執(zhí)行回調(diào)函數(shù)。
回到剛剛那個(gè)例子:
// 解決方法一
useEffect(() =>
if (timer.current)
clearInterval(timer.current);
timer.current = setInterval(() =>
console.log("setInterval:" count);
1000);
, count );
它第一次執(zhí)行的時(shí)候,執(zhí)行 useState,count 為 0。執(zhí)行 useEffect,執(zhí)行其回調(diào)中的邏輯,啟動(dòng)定時(shí)器,每隔 1s 輸出 setInterval: 0。
當(dāng)我點(diǎn)擊按鈕使 count 增加 1 的時(shí)候,整個(gè)函數(shù)式組件重新渲染,這個(gè)時(shí)候前一個(gè)執(zhí)行的鏈表已經(jīng)存在了。useState 將 Hook 對(duì)象 上保存的狀態(tài)置為 1, 那么此時(shí) count 也為 1 了。執(zhí)行 useEffect,其依賴項(xiàng)為空,不執(zhí)行回調(diào)函數(shù)。但是之前的回調(diào)函數(shù)還是在的,它還是會(huì)每隔 1s 執(zhí)行 console.log("setInterval:", count);,但這里的 count 是之前第一次執(zhí)行時(shí)候的 count 值,因?yàn)樵诙〞r(shí)器的回調(diào)函數(shù)里面被引用了,形成了閉包一直被保存。
解決的方法
解決方法一:給 useEffect 設(shè)置依賴項(xiàng),重新執(zhí)行函數(shù),設(shè)置新的定時(shí)器,拿到最新值。
constcount setCount = useState(0);
useEffect(() =>
setInterval(() =>
console.log("setInterval:" count);
1000);
, );
解決方法二:使用 useRef。useRef 返回一個(gè)可變的 ref 對(duì)象,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)。
useRef 創(chuàng)建的是一個(gè)普通 Javascript 對(duì)象,而且會(huì)在每次渲染時(shí)返回同一個(gè) ref 對(duì)象,當(dāng)我們變化它的 current 屬性的時(shí)候,對(duì)象的引用都是同一個(gè),所以定時(shí)器中能夠讀到最新的值。
const lastCount = useRef(count);
// 解決方法二
useEffect(() =>
setInterval(() =>
console.log("setInterval:" lastCount.current);
1000);
, );
return (
<div>
count count
<br />
<button
onClick= () =>
setCount((val) => val + 1);
// +1
lastCount.current += 1;
>
增加 1
</button>
</div>
);
useRef => useLatest
終于回到我們 ahooks 主題,基于上述的第二種解決方案,useLatest 這個(gè) hook 隨之誕生。它返回當(dāng)前最新值的 Hook,可以避免閉包問(wèn)題。實(shí)現(xiàn)原理很簡(jiǎn)單,只有短短的十行代碼,就是使用 useRef 包一層:
import 'react';useRef from
// 通過(guò) useRef,保持每次獲取到的都是最新的值
function useLatest<T>(value T)
const ref = useRef(value);
ref.current = value;
return ref;
export default useLatest;
useEvent => useMemoizedFn
React 中另一個(gè)場(chǎng)景,是基于 useCallback 的。
constcount setCount = useState(0);
const callbackFn = useCallback(() =>
console.log(`Current count is $ count `);
, );
以上不管,我們的 count 的值變化成多少,執(zhí)行 callbackFn 打印出來(lái)的 count 的值始終都是 0。這個(gè)是因?yàn)榛卣{(diào)函數(shù)被 useCallback 緩存,形成閉包,從而形成閉包陷阱。
那我們?cè)趺唇鉀Q這個(gè)問(wèn)題呢?官方提出了 useEvent。它解決的問(wèn)題:如何同時(shí)保持函數(shù)引用不變與訪問(wèn)到最新?tīng)顟B(tài)。使用它之后,上面的例子就變成了。
const callbackFn = useEvent(() =>
console.log(`Current count is $ count `);
);
在這里我們不細(xì)看這個(gè)特性,實(shí)際上,在 ahooks 中已經(jīng)實(shí)現(xiàn)了類似的功能,那就是 useMemoizedFn。
useMemoizedFn 是持久化 function 的 Hook,理論上,可以使用 useMemoizedFn 完全代替 useCallback。使用 useMemoizedFn,可以省略第二個(gè)參數(shù) deps,同時(shí)保證函數(shù)地址永遠(yuǎn)不會(huì)變化。以上的問(wèn)題,通過(guò)以下的方式就能輕松解決:
const memoizedFn = useMemoizedFn(() =>
console.log(`Current count is $ count `);
);
Demo 地址[5]
我們來(lái)看下它的源碼,可以看到其還是通過(guò) useRef 保持 function 引用地址不變,并且每次執(zhí)行都可以拿到最新的 state 值。
function useMemoizedFn<T extends noop>(fn T)
// 通過(guò) useRef 保持其引用地址不變,并且值能夠保持值最新
const fnRef = useRef<T>(fn);
fnRef.current = useMemo(() => fn fn );
// 通過(guò) useRef 保持其引用地址不變,并且值能夠保持值最新
const memoizedFn = useRef<PickFunction<T>>();
if (!memoizedFn.current)
// 返回的持久化函數(shù),調(diào)用該函數(shù)的時(shí)候,調(diào)用原始的函數(shù)
memoizedFn.current = function (this ...args)
return fnRef.current.apply(this args);
;
return memoizedFn.current as T;
總結(jié)與思考
React 自從引入 hooks,雖然解決了 class 組件的一些弊端,比如邏輯復(fù)用需要通過(guò)高階階段層層嵌套等。但是也引入了一些問(wèn)題,比如閉包問(wèn)題。
這個(gè)是 React 的 Function Component State 管理導(dǎo)致的,有時(shí)候會(huì)讓開(kāi)發(fā)者產(chǎn)生疑惑。開(kāi)發(fā)者可以通過(guò)添加依賴或者使用 useRef 的方式進(jìn)行避免。
ahooks 也意識(shí)到了這個(gè)問(wèn)題,通過(guò) useLatest 保證獲取到最新的值和 useMemoizedFn 持久化 function 的方式,避免類似的閉包陷阱。
值得一提的是 useMemoizedFn 是 ahooks 輸出函數(shù)的標(biāo)準(zhǔn),所有的輸出函數(shù)都使用 useMemoizedFn 包一層。另外輸入函數(shù)都使用 useRef 做一次記錄,以保證在任何地方都能訪問(wèn)到最新的函數(shù)。
參考
從react hooks“閉包陷阱”切入,淺談react hooks[6]
React官方團(tuán)隊(duì)出手,補(bǔ)齊原生Hook短板[7]
參考資料
[1]詳情: https://github.com/GpingFeng/hooks
[2]大家都能看得懂的源碼(一)ahooks 整體架構(gòu)篇: https://juejin.cn/post/7105396478268407815
[3]如何使用插件化機(jī)制優(yōu)雅的封裝你的請(qǐng)求hook : https://juejin.cn/post/7105733829972721677
[4]代碼示例: https://codesandbox.io/s/ji-chu-yong-fa-forked-wpk1s6?file=/App.tsx:0-487
[5]Demo 地址: https://codesandbox.io/s/ji-chu-yong-fa-forked-7pkp1r?file=/App.tsx
[6]從react hooks“閉包陷阱”切入,淺談react hooks: https://juejin.cn/post/6844904193044512782
[7]React官方團(tuán)隊(duì)出手,補(bǔ)齊原生Hook短板: https://segmentfault.com/a/1190000041798153