React官方團(tuán)隊(duì)出手,補(bǔ)齊原生Hook短板

源?/?魔術(shù)師卡頌? ? ? ??文/ 卡頌
我們知道,Hooks使用時(shí)存在所謂的「閉包陷阱」,考慮如下代碼:
function?Chat()?{
??const?[text,?setText]?=?useState('');
??const?onClick?=?useCallback(()?=>?{
????sendMessage(text);
??},?[]);
??return?<SendButton?onClick={onClick}?/>;
}
我們期望點(diǎn)擊后sendMessage能傳遞text的最新值。
然而實(shí)際上,由于回調(diào)函數(shù)被useCallback緩存,形成閉包,所以點(diǎn)擊的效果始終是sendMessage('')。
這就是「閉包陷阱」。
以上代碼的一種解決方式是「為useCallback增加依賴項(xiàng)」:
const?onClick?=?useCallback(()?=>?{
??sendMessage(text);
},?[text]);
但是這么做了后,每當(dāng)依賴項(xiàng)(text)變化,useCallback會(huì)返回一個(gè)全新的onClick引用,這就失去了useCallback「緩存函數(shù)引用」的作用。
「閉包陷阱」的出現(xiàn),加大了Hooks的上手門檻,也讓開發(fā)者更容易寫出有bug的代碼。
現(xiàn)在,React官方團(tuán)隊(duì)要出手解決這個(gè)問題。
useEvent
解決方式是引入一個(gè)新的原生Hook?——?useEvent。
他用于定義一個(gè)函數(shù),這個(gè)函數(shù)有2個(gè)特性:
在組件多次
render時(shí)保持引用一致函數(shù)內(nèi)始終能獲取到最新的
props與state
上面的例子使用useEvent改造后:
function?Chat()?{
??const?[text,?setText]?=?useState('');
??const?onClick?=?useEvent(()?=>?{
????sendMessage(text);
??});
??return?<SendButton?onClick={onClick}?/>;
}
在Chat組件多次render時(shí),onClick始終指向同一個(gè)引用。
并且onClick觸發(fā)時(shí)始終能獲取到text的最新值。
之所以叫useEvent,是因?yàn)?code style="margin-right: 2px;margin-left: 2px;padding: 2px 4px;font-size: 14px;border-radius: 4px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(145, 109, 213);font-weight: bolder;background-image: none;background-position: initial;background-size: initial;background-repeat: initial;background-attachment: initial;background-origin: initial;background-clip: initial;">React團(tuán)隊(duì)認(rèn)為這個(gè)Hook的主要應(yīng)用場(chǎng)景是:「封裝事件處理函數(shù)」。
useEvent的實(shí)現(xiàn)
useEvent的實(shí)現(xiàn)并不困難,代碼類似如下:
function?useEvent(handler)?{
??const?handlerRef?=?useRef(null);
??//?視圖渲染完成后更新`handlerRef.current`指向
??useLayoutEffect(()?=>?{
????handlerRef.current?=?handler;
??});
??//?用useCallback包裹,使得render時(shí)返回的函數(shù)引用一致
??return?useCallback((...args)?=>?{
????const?fn?=?handlerRef.current;
????return?fn(...args);
??},?[]);
}
整體包括兩部分:
返回一個(gè)沒有依賴項(xiàng)的 useCallback,使得每次render時(shí)函數(shù)的引用一致
useCallback((...args)?=>?{
??const?fn?=?handlerRef.current;
??return?fn(...args);
},?[]);
在合適的時(shí)機(jī)更新 handlerRef.current,使得實(shí)際執(zhí)行的函數(shù)始終是最新的引用
與開源Hooks的差異
很多開源Hooks庫已經(jīng)實(shí)現(xiàn)類似功能(比如ahooks中的useMemoizedFn)
useEvent與這些開源實(shí)現(xiàn)的差異主要體現(xiàn)在:
useEvent定位于「處理事件回調(diào)函數(shù)」這一單一場(chǎng)景,而useMemoizedFn定位于「緩存各種函數(shù)」。
那么問題來了,既然功能類似,那useEvent為什么要限制自己的使用場(chǎng)景呢?
答案是:為了更穩(wěn)定。
useEvent能否獲取到最新的state與props取決于handlerRef.current更新的時(shí)機(jī)。
在上面模擬實(shí)現(xiàn)中,useEvent更新handlerRef.current的邏輯放在useLayoutEffect回調(diào)中進(jìn)行。
這就保證了handlerRef.current始終在「視圖完成渲染」后再更新:
useLayoutEffect(()?=>?{
??handlerRef.current?=?handler;
});
而「事件回調(diào)」觸發(fā)的時(shí)機(jī)顯然在「視圖完成渲染」之后,所以能夠穩(wěn)定獲取到最新的state與props。
注:源碼內(nèi)的實(shí)際更新時(shí)機(jī)會(huì)更早些,但不影響這里的結(jié)論
再來看看ahooks中的useMemoizedFn,fnRef.current的更新時(shí)機(jī)是「useMemoizedFn執(zhí)行時(shí)」(即「組件render時(shí)」):
function?useMemoizedFn<T?extends?noop>(fn:?T)?{
??const?fnRef?=?useRef(fn);
??//?更新fnRef.current
??fnRef.current?=?useMemo(()?=>?fn,?[fn]);
??//?...省略代碼
}
當(dāng)React18啟用「并發(fā)更新」后,組件render的次數(shù)、時(shí)機(jī)并不確定。
所以useMemoizedFn中fnRef.current的更新時(shí)機(jī)也是不確定的。
這就增加了在「并發(fā)更新」下使用時(shí)潛在的風(fēng)險(xiǎn)。
可以說,useEvent通過限制handlerRef.current更新時(shí)機(jī),進(jìn)而限制應(yīng)用場(chǎng)景,最終達(dá)到穩(wěn)定的目的。
總結(jié)
useEvent當(dāng)前還處于RFC(Request For Comments)[1]階段。
很多熱心的開發(fā)者對(duì)這個(gè)Hook的命名提出了建議,比如:useStableCallback:

又比如:useLatestClosure:

從這些命名看,他們顯然擴(kuò)大了useEvent的應(yīng)用場(chǎng)景。
經(jīng)過本文的分析我們知道,「擴(kuò)大應(yīng)用場(chǎng)景」意味著「增加開發(fā)者使用時(shí)出錯(cuò)的風(fēng)險(xiǎn)」。
end

頂級(jí)程序員:topcoding
做最好的程序員社區(qū):Java后端開發(fā)、Python、大數(shù)據(jù)、AI
一鍵三連「分享」、「點(diǎn)贊」和「在看」
