React useEvent:磚家說的沒問題
之前寫了一篇文章《React Hooks 使用誤區,駁官方文檔 [1]》,文中拋出了兩個觀點:
-
不是所有的依賴都必須放到依賴數組中
-
deps 參數不能緩解閉包問題
這兩個觀點引起了劇烈的討論,當然大多數人還是持反對意見的,甚至質疑我不會用 Hooks,(⊙o⊙)… 我想說我寫的 Hooks 比你喫的鹽都多(開玩笑 😋 ~)
然後呢,知乎上來了個提問《如何看待《React Hooks 使用誤區,駁官方文檔》?[2]》,大家依舊是討論激烈,甚至 #黃玄 大佬也親自來回答了。
很多同學極力反對我的觀點,剛開始我還想一爭高下,後來實在沒精力一個一個對線。
這不,React 官方來幫我助陣了?React 官方爲啥出 useEvent?就是發現以前要求的依賴寫法,實在有太大問題,不加一個新的 API,官方示例都沒法寫了 🙂。
以前一直覺得 React Hooks 教程,包括 Dan 寫的 useEffect 教程 [3],都只是寫了基礎場景,對於稍微複雜點的場景,都避而不談。因爲這些複雜場景,在之前的規則下,確實是沒法玩。
什麼是 useEvent
關於 useEvent 是什麼,官方 RFC[4] 文檔有非常詳細的解釋,並且目前社區上也有非常多的文章介紹(其實很多介紹都是有問題的)。接下來用一個官方文檔上的一個例子,來認識一下 useEvent。需求很簡單,我們希望 url 變化的時候,上報下當前 url
和 username
。
function Page({ route, currentUser }) {
useEffect(() => {
logAnalytics('visit_page', route.url, currentUser.name);
}, [route.url]);
// ...
}
如上代碼,會有 warning,告訴我們 currentUser.name
要放到 deps 中。修正後代碼是這樣
function Page({ route, currentUser }) {
useEffect(() => {
logAnalytics('visit_page', route.url, currentUser.name);
}, [route.url, currentUser.name]);
// ...
}
但這樣明顯滿足不了我們的業務需求,因爲 currentUser.name
變化後,也觸發了上報請求。
很多槓精就問,爲啥你的需求要這樣設計?爲啥
currentUser.name
變化後不要上報?你的需求不合理吧?這個你去問 dan 吧~
以前的解決方案可能有兩個:
-
忽略警告,把
eslint-plugin-react-hooks
卸載掉 -
通過 ref 來標記
currentUser.name
function Page({ route, currentUser }) {
const ref = useRef(currentUser.name);
ref.current = currentUser.name;
useEffect(() => {
logAnalytics('visit_page', route.url, ref.current);
}, [route.url]);
// ...
}
兩個方案都有缺點:
-
打破了所謂的 React 對 deps 的限制規則
-
寫法太麻煩,項目複雜後要定義無數個 ref
基於 useEvent 改造起來就很簡單了
function Page({ route, currentUser }) {
// ✅ Stable identity
const onVisit = useEvent(visitedUrl => {
logAnalytics('visit_page', visitedUrl, currentUser.name);
});
useEffect(() => {
onVisit(route.url);
}, [route.url]); // ✅ Re-runs only on route change
// ...
}
useEvent 會將一個函數「持久化」,同時可以保證函數內部的變量引用永遠是最新的。如果你用過 ahooks 的 useMemoizedFn
,實現的效果是幾乎一致的。再強調下 useEvent 的兩個特性:
-
函數地址永遠是不變的
-
函數內引用的變量永遠是最新的
useEvent 可以用來代替 useCallback,以前這樣寫,在 text
變化的時候,函數地址會變化。
function Chat() {
const [text, setText] = useState('');
// 🟡 A different function whenever `text` changes
const onClick = useCallback(() => {
sendMessage(text);
}, [text]);
return <SendButton onClick={onClick} />;
}
通過 useEvent 代替 useCallback 後,不用寫 deps 函數了,並且函數地址永遠是固定的,text
也永遠是最新的。
function Chat() {
const [text, setText] = useState('');
// ✅ Always the same function (even if `text` changes)
const onClick = useEvent(() => {
sendMessage(text);
});
return <SendButton onClick={onClick} />;
}
useEvent 是怎麼實現的
useEvent 的實現原理比較簡單,但現在看到的社區上的介紹文章幾乎都有問題。
// (!) Approximate behavior
function useEvent(handler) {
const handlerRef = useRef(null);
// In a real implementation, this would run before layout effects
useLayoutEffect(() => {
handlerRef.current = handler;
});
return useCallback((...args) => {
// In a real implementation, this would throw if called during render
const fn = handlerRef.current;
return fn(...args);
}, []);
}
上面的代碼是官方提供的一個示例代碼,需要重點注意這句註釋 In a real implementation, this would run before layout effects
,翻譯過來就是 “在真實的實現中,這裏用的 Hooks 執行時機在 useLayoutEffect
之前”。
這裏一定是不能用 useLayoutEffect
來更新 ref
的,因爲子組件的 useLayoutEffect
比父組件的執行更早,如果這樣用的話,子組件的 useLayoutEffect
中訪問到的 ref
一定是舊的。
所以官方爲了實現 useEvent
,一定是要加一個在 useLayoutEffect
之前執行的 Hooks 的,並且這個 Hooks 應該不會開放給普通用戶使用的。
另外 React 要求不要在 render 中直接調用 useEvent
返回的函數,原理也是一樣的,在 render 中訪問的函數一定是舊的,因爲 useLayoutEffect
還沒執行呢。
useMemoizedFn 和 useEvent 的差異
在 React 18 之前,社區上有很多類似 useEvent
的實現,比如 ahooks[5] 的 useMemoizedFn,類似下面這樣
function useMemoizedFn(fn) {
const fnRef = useRef(fn);
fnRef.current = useMemo(() => fn, [fn]);
return useCallback((...args) => {
return fnRef.current.apply(args);
}, []);
}
之前很多同學問,爲啥不用 useLayoutEffect
,是不是有問題?現在應該明白了吧?我們需要一個比useLayoutEffect
執行更早的 Hooks,很遺憾的是之前更沒有,所以只能放到 render 中。
爲什麼之前官方沒有提供類似的 Hooks?useMemoizedFn 有問題嗎?之前 React Issue #16956[6] 上對類似的封裝做了很多討論,官方的態度一直是 “在 concurrent 下可能會存在問題” ,也就是官方也喫不準未來會不會出問題。隨着 React 18 發佈,concurrent 模式穩定之後,官方發現,這種寫法不會有問題,索性就自己提供了一個。
在 React 18 之前,因爲沒有 concurrent,所以 useMemoizedFn 不會有任何問題。在 React 18 之後,我目前也沒看到有什麼問題。不過爲了穩妥起見,後面 ahooks 的 useMemoizedFn 會做一次升級,向官方的 useEvent 看齊。
最後用知乎上一個同學的評論結尾 “面多了加水,水多了加面”。
參考資料
[1]
React Hooks 使用誤區,駁官方文檔: https://zhuanlan.zhihu.com/p/450513902
[2]
如何看待《React Hooks 使用誤區,駁官方文檔》?: https://www.zhihu.com/question/508780830
[3]
useEffect 教程: https://overreacted.io/a-complete-guide-to-useeffect/
[4]
RFC: https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md
[5]
ahooks: https://github.com/alibaba/hooks
[6]
#16956: https://github.com/facebook/react/issues/16956
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/-6bQKIjH6WPcfuiCFtsjng