React 新出的 useEvent,到底是什麼東西?
useEvent 要解決一個問題:如何同時保持函數引用不變與訪問到最新狀態。
本週我們結合 RFC 原文與解讀文章 What the useEvent React hook is (and isn't) 一起了解下這個提案。
借用提案裏的代碼,一下就能說清楚 useEvent 是個什麼東西:
function Chat() {
const [text, setText] = useState('');
// ✅ Always the same function (even if `text` changes)
const onClick = useEvent(() => {
sendMessage(text);
});
return <SendButton onClick={onClick} />;
}
onClick 既保持引用不變,又能在每次觸發時訪問到最新的 text 值。
爲什麼要提供這個函數,它解決了什麼問題,在概述裏慢慢道來。
概述
定義一個訪問到最新 state 的函數不是什麼難事:
function App() {
const [count, setCount] = useState(0)
const sayCount = () => {
console.log(count)
}
return <Child onClick={sayCount} />
}
但 sayCount 函數引用每次都會變化,這會直接破壞 Child 組件 memo 效果,甚至會引發其更嚴重的連鎖反應(Child 組件將 onClick 回調用在 useEffect 裏時)。
想要保證 sayCount 引用不變,我們就需要用 useCallback 包裹:
function App() {
const [count, setCount] = useState(0)
const sayCount = useCallback(() => {
console.log(count)
}, [count])
return <Child onClick={sayCount} />
}
但即便如此,我們僅能保證在 count 不變時,sayCount 引用不變。如果想保持 sayCount 引用穩定,就要把依賴 [count] 移除,這會導致訪問到的 count 總是初始值,邏輯上引發了更大問題。
一種無奈的辦法是,維護一個 countRef,使其值與 count 保持同步,在 sayCount 中訪問 countRef:
function App() {
const [count, setCount] = useState(0)
const countRef = React.useRef()
countRef.current = count
const sayCount = useCallback(() => {
console.log(countRef.current)
}, [])
return <Child onClick={sayCount} />
}
這種代碼能解決問題,但絕對不推薦,原因有二:
-
每個值都要加一個配套 Ref,非常冗餘。
-
在函數內直接同步更新 ref 不是一個好主意,但寫在
useEffect裏又太麻煩。
另一種辦法就是自創 hook,如 useStableCallback,這本質上就是這次提案的主角 - useEvent:
function App() {
const [count, setCount] = useState(0)
const sayCount = useEvent(() => {
console.log(count)
})
return <Child onClick={sayCount} />
}
所以 useEvent 的內部實現很可能類似於自定義 hook useStableCallback。在提案內也給出了可能的實現思路:
// (!) 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);
}, []);
}
其實很好理解,我們將需求一分爲二看:
-
既然要返回一個穩定引用,那最後返回的函數一定使用
useCallback並將依賴數組置爲[]。 -
又要在函數執行時訪問到最新值,那麼每次都要拿最新函數來執行,所以在 Hook 裏使用 Ref 存儲每次接收到的最新函數引用,在執行函數時,實際上執行的是最新的函數引用。
注意兩段註釋,第一個是 useLayoutEffect 部分實際上要比 layoutEffect 執行時機更提前,這是爲了保證函數在一個事件循環中被直接消費時,可能訪問到舊的 Ref 值;第二個是在渲染時被調用時要拋出異常,這是爲了避免 useEvent 函數被渲染時使用,因爲這樣就無法數據驅動了。
精讀
其實 useEvent 概念和實現都很簡單,下面我們聊聊提案裏一些有意思的細節吧。
爲什麼命名爲 useEvent
提案裏提到,如果不考慮名稱長短,完全用功能來命名的話,useStableCallback 或 useCommittedCallback 會更加合適,都表示拿到一個穩定的回調函數。但 useEvent 是從使用者角度來命名的,即其生成的函數一般都被用於組件的回調函數,而這些回調函數一般都有 “事件特性”,比如 onClick、onScroll,所以當開發者看到 useEvent 時,可以下意識提醒自己在寫一個事件回調,還算比較直觀。(當然我覺得主要原因還是爲了縮短名稱,好記)
值並不是真正意義上的實時
雖然 useEvent 可以拿到最新值,但和 useCallback 拿 ref 還是有區別的,這個差異體現在:
function App() {
const [count, setCount] = useState(0)
const sayCount = useEvent(async () => {
console.log(count)
await wait(1000)
console.log(count)
})
return <Child onClick={sayCount} />
}
await 前後輸出值一定是一樣的,在實現上,count 值僅是調用時的快照,所以函數內異步等待時,即便外部又把 count 改了,當前這次函數調用還是拿不到最新的 count,而 ref 方法是可以的。在理解上,爲了避免夜長夢多,回調函數儘量不要寫成異步的。
useEvent 也救不了手殘
如果你堅持寫出 onSomething={cond ? handler1 : handler2} 這樣的代碼,那麼 cond 變化後,傳下去的函數引用也一定會變化,這是 useEvent 無論如何也避免不了的,也許解救方案是 Lint and throw error。
其實將 cond ? handler1 : handler2 作爲一個整體包裹在 useEvent 就能解決引用變化的問題,但除了 Lint,沒有人能防止你繞過它。
可以用自定義 hook 代替 useEvent 實現嗎?
不能。雖然提案裏給了一個近似解決方案,但實際上存在兩個問題:
-
在賦值 ref 時,
useLayoutEffect時機依然不夠提前,如果值變化後理解訪問函數,拿到的會是舊值。 -
生成的函數被用在渲染並不會給出錯誤提示。
總結
useEvent 顯然又給 React 增加了一個官方概念,在結結實實增加了理解成本的同時,也補齊了 React Hooks 在實踐中缺失的重要一環,無論你喜不喜歡,問題就在那,解法也給了,挺好。
討論地址是:精讀《React useEvent RFC》· Issue #415 · dt-fe/weekly
如果你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
版權聲明:自由轉載 - 非商用 - 非衍生 - 保持署名(創意共享 3.0 許可證)
-
歡迎
長按圖片加 ssh 爲好友,我會第一時間和你分享前端行業趨勢,學習途徑等等。2022 陪你一起度過! -
-
關注公衆號後,在首頁:
回覆
指南,高級前端、算法學習路線,是我自己一路走來的實踐。回覆
簡歷,大廠簡歷編寫指南,是我看了上百份簡歷後總結的心血。回覆
面經,大廠面試題,集結社區優質面經,助你攀登高峯。
前端從進階到入院 我是 ssh,只想用最簡單的方式把原理講明白。wx:sshsunlight,分享前端的前沿趨勢和一些有趣的事情。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/npKuhfHft9dTLBlG0hjAGA