React 如何原生實現防抖?
作者:卡頌
簡介:《React 技術揭祕》作者
來源:SegmentFault 思否社區
大家好,我卡頌。
作爲前端,想必你對防抖(debounce)、節流(throttle)這兩個概念不陌生。
在 React18 中,基於新的併發特性,React 原生實現了防抖的功能。
今天我們來聊聊這是如何實現的。
useTransition Demo
useTransition 是一個新增的原生 Hook,用於以較低優先級執行一些更新。
在我們的 Demo 中有 ctn 與 num 兩個狀態,其中 ctn 與輸入框的內容受控。
當觸發輸入框 onChange 事件時,會同時觸發 ctn 與 num 狀態變化。其中觸發 num 狀態變化的方法(即 updateNum)被包裹在 startTransition 中:
function App() {
const [ctn, updateCtn] = useState('');
const [num, updateNum] = useState(0);
const [isPending, startTransition] = useTransition();
return (
<div >
<input value={ctn} onChange={({target: {value}}) => {
updateCtn(value);
startTransition(() => updateNum(num + 1))
}}/>
<BusyChild num={num}/>
</div>
);
}
num 會作爲 props 傳遞給 BusyChild 組件。在 BusyChild 中通過 while 循環人爲增加組件 render 所消耗的時間:
const BusyChild = React.memo(({num}: {num: number}) => {
const cur = performance.now();
// 增加render的耗時
while (performance.now() - cur < 300) {}
return <div>{num}</div>;
})
所以,在輸入框輸入內容時能明顯感到卡頓。
在線示例地址:https://codesandbox.io/s/immutable-glade-u0m6vv
按理說,onChange 中會同時觸發 ctn 與 num 的狀態變化,他們在視圖中的顯示應該是同步的。
然而實際上,輸入框連續輸入一段文字(即 ctn 的狀態變化連續展示在視圖中)後,num 纔會變化一次。
如下圖,初始時輸入框沒有內容,num 爲 0:
輸入框輸入很長一段文字後,num 才變爲 1:
這種效果就像:被 startTransition 包裹的更新都有防抖的效果一樣。
這是如何實現的呢?
什麼是 lane
在 React18 中有一套更新優先級機制,不同地方觸發的更新擁有不同優先級。優先級的定義依據是符合用戶感知的,比如:
-
用戶不希望輸入框輸入文字會有卡頓,所以 onChange 事件中觸發的更新是同步優先級(最高優)
-
用戶可以接受請求發出到返回之間有等待時間,所以 useEffect 中觸發的更新是默認優先級
那麼優先級怎麼表示呢?用一個 31 位的二進制,被稱爲 lane。
比如同步優先級和默認優先級定義如下:
const SyncLane = 0b0000000000000000000000000000001;
const DefaultLane = 0b0000000000000000000000000010000;
數值越小優先級越大,即 SyncLane < DefaultLane。
那麼 React 每次更新是不是選擇一個優先級,然後執行所有組件中這個優先級對應的更新呢?
不是。如果每次更新只能選擇一個優先級,那靈活性就太差了。
所以實際情況是:每次更新,React 會選擇一到多個 lane 組成一個批次,然後執行所有組件中包含在這個批次中的 lane 對應的更新
這種組成批次的 lane 被稱爲 lanes。
比如,如下代碼將 SyncLane 與 DefaultLane 合成 lanes:
// 用“按位或”操作合併lane
const lanes = SyncLane | DefaultLane;
可以看到,lane 機制本質上就是各種位運算,可以設計的很靈活。
在此基礎上,有一套被稱爲 entangle(糾纏)的機制。
entangle 指一種 lane 之間的關係,如果 laneA 與 laneB 糾纏,那麼某次更新 React 選擇了 laneA,則必須帶上 laneB。
也就是說 laneA 與 laneB 糾纏在一塊,同生共死了。
除此之外,如果 laneA 與 laneC 糾纏,此時 laneC 與 laneB 糾纏,那麼 laneA 也會與 laneB 糾纏。
那麼 entangle 機制與 useTransition 有什麼關係呢?
被 startTransition 包裹的回調中觸發的更新,優先級爲 TransitionLanes 中的一個。
TransitionLanes 中包括 16 個 lane,分別是 TransitionLane1 到 TransitionLane16:
而 transition 相關 lane 會發生糾纏。
在我們的 Demo 中,每次 onChange 執行,都會創建兩個更新:
onChange={({target: {value}}) => {
updateCtn(value);
startTransition(() => updateNum(num + 1))
}
其中:
-
updateCtn(value) 由於在 onChange 中觸發,優先級爲 SyncLane
-
updateNum(num + 1) 由於在 startTransition 中觸發,優先級爲 TransitionLanes 中的某一個
當在輸入框中反覆輸入文字時,以上過程會反覆執行,區別是:
-
SyncLane 由於是最高優先級,會被執行,所以我們會看到輸入框中內容變化
-
TransitionLanes 相關 lane 優先級比 SyncLane 低,暫時不會執行,同時他們會產生糾纏
爲了防止某次更新由於優先級過低,一直無法執行,React 有個過期機制:每個更新都有個過期時間,如果在過期時間內都沒有執行,那麼他就會過期。
過期後的更新會同步執行(也就是說他的優先級變得和 SyncLane 一樣)
在我們的例子中,startTransition(() => updateNum(num + 1)) 會產生很多糾纏在一塊的 TransitionLanes 相關 lane。
過了一段時間,其中某個 lane 過期了,於是他優先級提高到和 SyncLane 一樣,立刻執行。
又由於這個 lane 與其他 TransitionLanes 相關 lane 糾纏在一起,所以他們會被一起執行。
這就表現爲:在輸入框一直輸入內容,但是 num 在視圖中顯示的數字過了會兒才變化。
總結
今天我們聊了 useTransition 內部的一些實現,涉及到:
-
lane 模型
-
entangle 機制
-
更新過期機制
最有意思的是,由於不同電腦性能不同,瀏覽器幀率會變動,所以在不同電腦中 React 會動態調節防抖的效果。
這就相當於不需要你手動設置 debounce 的時間參數,React 會根據電腦性能動態調整。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/FXgMDOoA97OKwzRWcI4zoA