React如何原生實現防抖?

卡頌 發表於 2022-05-15
React

大家好,我卡頌。

作為前端,想必你對防抖(debounce)、節流(throttle)這兩個概念不陌生。

React18中,基於新的併發特性,React原生實現了防抖的功能。

今天我們來聊聊這是如何實現的。

歡迎加入人類高質量前端框架群,帶飛

useTransition Demo

useTransition是一個新增的原生Hook,用於以較低優先順序執行一些更新

在我們的Demo中有ctnnum兩個狀態,其中ctn與輸入框的內容受控。

當觸發輸入框onChange事件時,會同時觸發ctnnum狀態變化。其中觸發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>;
})

所以,在輸入框輸入內容時能明顯感到卡頓。

React如何原生實現防抖?

線上示例地址

按理說,onChange中會同時觸發ctnnum的狀態變化,他們在檢視中的顯示應該是同步的。

然而實際上,輸入框連續輸入一段文字(即ctn的狀態變化連續展示在檢視中)後,num才會變化一次。

如下圖,初始時輸入框沒有內容,num為0:

React如何原生實現防抖?

輸入框輸入很長一段文字後,num才變為1:

React如何原生實現防抖?

這種效果就像:被startTransition包裹的更新都有防抖的效果一樣。

這是如何實現的呢?

什麼是lane

React18中有一套更新優先順序機制,不同地方觸發的更新擁有不同優先順序。優先順序的定義依據是符合使用者感知的,比如:

  • 使用者不希望輸入框輸入文字會有卡頓,所以onChange事件中觸發的更新是同步優先順序(最高優)
  • 使用者可以接受請求發出到返回之間有等待時間,所以useEffect中觸發的更新是預設優先順序

那麼優先順序怎麼表示呢?用一個31位的二進位制,被稱為lane

比如同步優先順序預設優先順序定義如下:

const SyncLane =    0b0000000000000000000000000000001;
const DefaultLane = 0b0000000000000000000000000010000;

數值越小優先順序越大,即SyncLane < DefaultLane

那麼React每次更新是不是選擇一個優先順序,然後執行所有元件中這個優先順序對應的更新呢?

不是。如果每次更新只能選擇一個優先順序,那靈活性就太差了。

所以實際情況是:每次更新,React會選擇一到多個lane組成一個批次,然後執行所有元件中包含在這個批次中的lane對應的更新

這種組成批次的lane被稱為lanes

比如,如下程式碼將SyncLaneDefaultLane合成lanes

// 用“按位或”操作合併lane
const lanes = SyncLane | DefaultLane;

entangle機制

可以看到,lane機制本質上就是各種位運算,可以設計的很靈活。

在此基礎上,有一套被稱為entangle(糾纏)的機制。

entangle指一種lane之間的關係,如果laneAlaneB糾纏,那麼某次更新React選擇了laneA,則必須帶上laneB

也就是說laneAlaneB糾纏在一塊,同生共死了。

除此之外,如果laneAlaneC糾纏,此時laneClaneB糾纏,那麼laneA也會與laneB糾纏。

那麼entangle機制與useTransition有什麼關係呢?

startTransition包裹的回撥中觸發的更新,優先順序為TransitionLanes中的一個。

TransitionLanes中包括16個lane,分別是TransitionLane1TransitionLane16

React如何原生實現防抖?

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會根據電腦效能動態調整。