React如何原生實現(xiàn)防抖?
大家好,我卡頌。
作為前端,想必你對防抖(debounce)、節(jié)流(throttle)這兩個概念不陌生。
在React18中,基于新的并發(fā)特性,React原生實現(xiàn)了防抖的功能。
今天我們來聊聊這是如何實現(xiàn)的。
useTransition Dem
ouseTransition是一個新增的原生Hook,用于「以較低優(yōu)先級執(zhí)行一些更新」。
在我們的Demo中有ctn與num兩個狀態(tài),其中ctn與輸入框的內容受控。
當觸發(fā)輸入框onChange事件時,會同時觸發(fā)ctn與num狀態(tài)變化。其中「觸發(fā)num狀態(tài)變化的方法」(即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循環(huán)人為增加組件render所消耗的時間:
const BusyChild = React.memo(({num}: {num: number}) => {
const cur = performance.now();
// 增加render的耗時
while (performance.now() - cur < 300) {}
return <div>{num}</div>;
})
所以,在輸入框輸入內容時能明顯感到卡頓。
在線示例地址[1]。
按理說,onChange中會同時觸發(fā)ctn與num的狀態(tài)變化,他們在視圖中的顯示應該是同步的。
然而實際上,輸入框連續(xù)輸入一段文字(即ctn的狀態(tài)變化連續(xù)展示在視圖中)后,num才會變化一次。
如下圖,初始時輸入框沒有內容,num為0:
輸入框輸入很長一段文字后,num才變?yōu)?:
這種效果就像:被startTransition包裹的更新都有「防抖」的效果一樣。
這是如何實現(xiàn)的呢?
什么是lane
在React18中有一套「更新優(yōu)先級機制」,不同地方觸發(fā)的更新?lián)碛胁煌瑑?yōu)先級。優(yōu)先級的定義依據(jù)是符合用戶感知的,比如:
- 用戶不希望輸入框輸入文字會有卡頓,所以onChange事件中觸發(fā)的更新是同步優(yōu)先級(最高優(yōu))。
- 用戶可以接受請求發(fā)出到返回之間有等待時間,所以useEffect中觸發(fā)的更新是默認優(yōu)先級。
那么優(yōu)先級怎么表示呢?用一個31位的二進制,被稱為lane。
比如「同步優(yōu)先級」和「默認優(yōu)先級」定義如下:
const SyncLane = 0b0000000000000000000000000000001;
const DefaultLane = 0b0000000000000000000000000010000;
數(shù)值越小優(yōu)先級越大,即SyncLane < DefaultLane。
那么React每次更新是不是選擇一個優(yōu)先級,然后執(zhí)行所有組件中「這個優(yōu)先級對應的更新」呢?
不是。如果每次更新只能選擇一個優(yōu)先級,那靈活性就太差了。
所以實際情況是:每次更新,React會選擇一到多個lane組成一個批次,然后執(zhí)行所有組件中「包含在這個批次中的lane對應的更新」
這種組成批次的lane被稱為lanes。
比如,如下代碼將SyncLane與DefaultLane合成lanes:
// 用“按位或”操作合并lane
const lanes = SyncLane | DefaultLane;
entangle機制
可以看到,lane機制本質上就是各種位運算,可以設計的很靈活。
在此基礎上,有一套被稱為entangle(糾纏)的機制。
entangle指一種lane之間的關系,如果laneA與laneB糾纏,那么某次更新React選擇了laneA,則必須帶上laneB。
也就是說laneA與laneB糾纏在一塊,同生共死了。
除此之外,如果laneA與laneC糾纏,此時laneC與laneB糾纏,那么laneA也會與laneB糾纏。
那么entangle機制與useTransition有什么關系呢?
被startTransition包裹的回調中觸發(fā)的更新,優(yōu)先級為TransitionLanes中的一個。
TransitionLanes中包括16個lane,分別是TransitionLane1到TransitionLane16:
而transition相關lane會發(fā)生糾纏。
在我們的Demo中,每次onChange執(zhí)行,都會創(chuàng)建兩個更新:
onChange={({target: {value}}) => {
updateCtn(value);
startTransition(() => updateNum(num + 1))
}
其中:
- updateCtn(value)由于在onChange中觸發(fā),優(yōu)先級為SyncLane。
- updateNum(num + 1)由于在startTransition中觸發(fā),優(yōu)先級為TransitionLanes中的某一個。
當在輸入框中反復輸入文字時,以上過程會反復執(zhí)行,區(qū)別是:
- SyncLane由于是最高優(yōu)先級,會被執(zhí)行,所以我們會看到輸入框中內容變化。
- TransitionLanes相關lane優(yōu)先級比SyncLane低,暫時不會執(zhí)行,同時他們會產生糾纏。
為了防止某次更新由于優(yōu)先級過低,一直無法執(zhí)行,React有個「過期機制」:每個更新都有個過期時間,如果在過期時間內都沒有執(zhí)行,那么他就會過期。
過期后的更新會同步執(zhí)行(也就是說他的優(yōu)先級變得和SyncLane一樣)。
在我們的例子中,startTransition(() => updateNum(num + 1))會產生很多糾纏在一塊的TransitionLanes相關lane。
過了一段時間,其中某個lane過期了,于是他優(yōu)先級提高到和SyncLane一樣,立刻執(zhí)行。
又由于這個lane與其他TransitionLanes相關lane糾纏在一起,所以他們會被一起執(zhí)行。
這就表現(xiàn)為:在輸入框一直輸入內容,但是num在視圖中顯示的數(shù)字過了會兒才變化。
總結
今天我們聊了useTransition內部的一些實現(xiàn),涉及到:
- lane模型。
- entangle機制。
- 更新過期機制。
最有意思的是,由于不同電腦性能不同,瀏覽器幀率會變動,所以在不同電腦中React會動態(tài)調節(jié)防抖的效果。
這就相當于不需要你手動設置debounce的時間參數(shù),React會根據(jù)電腦性能動態(tài)調整。
參考資料
[1]在線示例地址:
??https://codesandbox.io/s/immutable-glade-u0m6vv?file=/src/App.js。??