React如何原生實現防抖?

語言: CN / TW / HK

大家好,我卡頌。

作為前端,想必你對防抖( 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>;
})

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

線上示例地址

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

然而實際上,輸入框連續輸入一段文字(即 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

比如,如下程式碼將 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

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