大家好,我卡頌。
作為前端,想必你對防抖(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>;
})
所以,在輸入框輸入內容時能明顯感到卡頓。
線上示例地址
按理說,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;
entangle機制
可以看到,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
會根據電腦效能動態調整。