发布于:2024-9-15最后更新:2024-9-15W.C. 00 min

type
status
date
slug
summary
tags
category
icon
password
我們過往有瞭解到說 React 可以根據任務的優先級去做任務調度,也就是說並不是所有的更新都會立即去做執行,而是會先去根據它們的重要性去調整編排。
那其中一個例子就是 performConcurrentWorkOnRoot(),它是一個在 React 內部所使用的函數,用來處理併發特性中的渲染任務。這個函數不會直接由開發者所調用,而是由 React 的調度器在適當時機自動調用。而它的目的是針對處理整個 fiber tree,而非單一 fiber 上的工作層級。
併發模式 (Concurrent Mode),是 React 18 的一個特性,它允許同時處理多個更新,以更有效地進行資源利用,避免不必要的渲染,提高應用的性能。併發模式下,每個 fiber 都能有自己的更新優先級,React 就依據這些優先級來決定處理任務的順序。
而我們這次要探討的 Lane 是 React 中用來表示更新優先級的一個概念。每個更新都會被分配到一個或多個 lane 上,lane 的編號代表了更新的優先級。像剛說到 React 根據 fiber 優先級去處理任務的邏輯,就是通過 Lane 的概念去實現的。
沒看很懂也沒關係,主要我們先有個初步的概念,下面將會有更詳細的解釋和範例來供讀者們做理解。

三個優先級系統

咱們先來看看 ensureRootIsScheduled(),這是調用調度方法的入口之一。
從上述程式中,我們可以看到調度優先級是通過以下方式得出的:
  1. 獲取最高的 Lane 優先級 - getHighestPriorityLane()
  1. 如果不是 SyncLane,則將 lane 映射到事件優先級,然後映射到調度器優先級。
因此可瞭解到,我們存在三種優先級系統:
  • 調度器優先級 - 用於在調度器中優先級任務
  • 事件優先級 - 標記用戶事件的優先級
  • 車道優先級 - 標記工作的優先級
它们用于不同的目的,且彼此分離,但存在映射逻辑。 關於事件系统這部分,我將來有機會再寫一篇文章講解,所以我們這裡就暫時不去涉及太多細節。

究竟什麼是 Lane?

如果你有了解過 setState() 的話,我們會知道,fiber 持有一個 hooks 的鏈表,對於狀態 hook,它有一個在更新(re-render)期間運行的更新隊列。
以下是創建更新的代碼(source)
是的,注意到一個叫做 lane 的字段了嗎? Lane 是用來標記更新的優先級,我們也可以說它用來標記了工作的優先級。
下面這些是 React 中所有的 lanes,這都只是一些數字,那我們用二進制形式更便於理解。仔細觀察並找出每個 1
Lane 顧名思義,就像路上一條條的車道一樣,車子是根據不同的速度去決定它在不同的車道上行駛。這也就意味著,車道編號越小,工作就越緊急,優先級就越高。 於是 SyncLane 在這裡就是 1
那在實現上我們可以看到說有很多 Lane,但我們也不可能去深入研究每一個,只需要去理解它一般的工作原理即可。

位運算

Lane 其實就只是數字,在 React 源碼中有很多的位運算,我們先熟悉一下。
以下是一些簡單示例。

childLanes

如果你對 React 在協調(reconciliation)過程中如何進行 bailout 有所了解,那應該也對 Lanes 和 childLanes 不是太陌生。
如果還不清楚什麼是 bailout 也沒關係,我來簡單解釋一下。 bailout 是一種最佳化手段,用來避免不必要的渲染。當 React 確定某個元件的輸出在連續的渲染中沒有變化時,它可以選擇跳過這個元件的渲染工作,這就是所謂的 bailout。
哪些地方會用到 bailout 呢?
  1. React.memo:這是一個高階元件,它對元件進行淺比較,如果元件的 props 沒有變化,它就防止元件重新渲染。
  1. shouldComponentUpdate:這是一個生命週期方法,React 會在元件更新前呼叫它。如果這個方法回傳 false,React 將跳過該元件的渲染。
  1. PureComponent:與 React.memo 類似,PureComponent 在 React 類別元件中提供了淺比較 props 和 state 的實作。如果 props 或 state 沒有變化,它將防止元件重新渲染。
  1. React.PureComponent:這是一個內建的類別元件,它會自動為你的元件實作 shouldComponentUpdate 方法,透過淺比較 props 和 state 來決定是否需要更新。
  1. bailout 位元:在 React 的協調演算法中,每個 fiber 節點都有一個標誌位,用來表示是否應該進行 bailout。如果確定組件的 props 或 state 沒有變化,這個位元就會被設定。
  1. 時間切片:React 的協調過程是可中斷的,這意味著它可以透過時間切片來檢查是否有 bailout 的機會,在長時間的渲染過程中提高效能。
  1. 子元件的 bailout:如果一個元件 bailout 了,它的子元件也會被檢查是否有 bailout 的機會。如果子組件的 props 沒有變化,它們同樣可以 bailout。
  1. Hooks 的 bailout:在使用函數元件和 Hooks 時,如果依賴項沒有變化,React 可以跳過函數元件內部的 Hooks 呼叫。
React 的 bailout 機制是自動的,開發者通常不需要手動介入。但透過使用如 React.memoPureComponent 或正確實現 shouldComponentUpdate 等技術,開發者可以明確地告訴 React 在什麼情況下可以跳過元件的渲染,從而優化應用的效能。
每個 fiber 都會知道:
  • 它自己的工作優先級 - lanes
  • 他的子 fiber 的工作優先級 - childLanes

再來看看 performConcurrentWorkOnRoot()

這是工作被調度和運作的基本流程:
  1. 取得 fiber tree 的 nextLanes
  1. 將其映射到調度器優先級
  1. 安排任務以協調
  1. 協調發生,處理從根開始的工作
神奇的地方在於實際的協調,那大概就是 lane 裡頭信息被使用的地方。
prepareFreshStack() 意味著重新開始協調,有一個指針(workInProgress)來跟蹤當前的 fiber,通常 React 會暫停並從之前的位置恢復。但在出現錯誤或一些奇奇怪怪的情況下,我們需要放棄當前所做的工作,並從頭開始做,這就是 fresh 的意思。
我們可以看到在 prepareFreshStack() 中,一些變量只是被重置了。 裡頭有很多關於 Lane 的變量:
  1. workInProgressRootRenderLanes
  1. subtreeRenderLanes
  1. workInProgressRootIncludedLanes
  1. workInProgressRootSkippedLanes
  1. workInProgressRootInterleavedUpdatedLanes
  1. workInProgressRootRenderPhaseUpdatedLanes
  1. workInProgressRootPingedLanes
OK,但現在我們不知道這些東西是幹什麼的,但是 workInProgressRootRenderLanes 看起來是挺簡單易懂。
也正如它的註釋所說,它是我們正在渲染的 Lanes。
有幾個地方中使用了它,例如這裡:
啊哈, 注意它是 requestUpdateLane()?可能有點難理解說上述函數中發生了些什麼,但很明顯的一點,當前渲染的 lanes 以某種方式去影響了渲染時調度的 lanes。
讓我們先回到 performConcurrentWorkOnRoot()
這東西決定了什麼 Lanes 會被處理,getNextLanes() 的邏輯其實相當複雜,我們這裡先挖個坑後跳過,只需要瞭解到在最基本的情況下,getNextLanes() 會選擇最高優先級的 Lane。
有趣的是,即使在併發模式下,也可能存在某些情況會回退到同步模式。 舉個例子來說,比如 Lanes 中包括了一些阻塞 Lanes,或某些 Lanes 已經過期之類的。
那這邊就也不再展開,暫且略過這些細節並繼續。

updateReducer()

正如我們先備知識所知,useState() 會映射到 mountState() 用於初始渲染,且在後續更新中 updateState()
狀態更新會發生在 updateState() 中。
内部使用 updateReducer()。(source
這一坨中,我們只去關注核心部分。
嘿,它循環遍歷更新並使用 isSubsetOfLanes 檢查 lanes,renderLanes 是在 renderWithHooks() 中設置、回溯,且根函數調用存在於 performUnitOfWork()。也就是說,這過程中的渲染決策最終是由 performUnitOfWork() 所控制的,這函數決定如何處理每個單元的工作,包括是否 bailout 某些組建的渲染。
啊嘶,結束。到目前為止這些內容,我們應該大致了解了 Lane 是如何工作的。

總結

  1. 當事件在 fiber 上發生時,創建帶有 Lane 信息的更新,由幾個因素所決定。
  1. 祖先 fiber 都被標記為 childLanes,所以對於任何 fiber,我們都可以得到子孫節點的 Lane 信息。
  1. 從根獲取最高優先級 Lane → 將其映射到調度器優先級 → 安排在調度器中協調 fiber tree 的任務。
  1. 在協調中,會選擇最高優先級 Lane 進行工作 - current rendering lanes。
  1. 遍歷 fiber tree,檢查 hooks 上的更新,運行包含在 rendering lanes 中的更新。
因此,我們就能夠分別運行單個 fiber 上的多個更新。

Lane 有什麼意義?

一個 Demo 砸臉比說一大堆解釋有時候更容易讓人有所體悟。

示例一 - 輸入被長列表阻塞

打開第一個 demo,輸入一些東西,你可以感覺到明顯的延遲,輸入字段沒有響應。
我們可以通過對 <Cells/> 的更新使用 startTransition() 來改善這種情況,看看第二個演示。

示例二 - 通過將重工作移到 transition lanes,輸入不被阻塞

打開第二個 demo 試試。
這裡使用的 tips 是 useDeferredValue(),它將更新置於 transition lanes 中。 關於這 API 的詳情,自己看文檔去😘
還可以打開 DevTools,你可以看到這兩個的區別。
對於第一個:
你可以看到只有一個 Lane - SyncLane,所以輸入和單元格更新在同一批次中處理。
而在第二個 Demo 中情況就有所不同。
我們可以看見說有兩個 Lane,第一個是用於輸入的 SyncLane,但對於 Cell 來說,他是 TransitionLane1

示例三 - 使用內部 API 去調度

第三個 Demo 也非常簡單易懂。
我們同時調用了兩次 setState(),但每次調用的更新優先級(Lane)不同,第一次調用是 InputContinuousLane,第二次調用是 SyncLane
那麼你覺得會發生什麼結果呢?
如果我们不考虑优先级,我们可能会认为它们是一起处理的,所以是 1 -> 20
實際結果是 1 -> 10 -> 20
打開 DevTools 並點擊按鈕,我們可以來看看發生了些什麼。
首先會先處理 SyncLane,所以 1 * 10 = 10,然後處理剩下的 Lane,注意一點是 SyncLane 的 hook 更新仍需要保持一致性,所以 (1 + 1) * 10 = 20
以上就是這次的內容,希望這有助於你更好理解 Lane 以及 React 的內部工作機制。

Python 語言與操作環境 | Python 程式設計入門 (EP.1)
Python 語言與操作環境 | Python 程式設計入門 (EP.1)
Announcement
type
status
date
slug
summary
tags
category
icon
password
🎉新版博客即將到來🎉
-- 感谢您的注意 ---
👏為您帶來愉快的體驗👏