系列文章:
- 【React 源碼閲讀】為什麼 React Hooks 不能用條件語句來執行?
- 【React 源碼閲讀】useCallback
- 【React 源碼閲讀】Scheduler
1 寫在前面
React 源碼裏的概念實在是太多了,以至於如果真的要能完全理解源碼的話,我們就不得不提前瞭解一部分知識,不然看源碼的時候完全就是抓瞎。
2 Fiber
2.1 為什麼要有 Fiber
想象一下,如果你手頭上的事情分別有:
- 帶娃👶(主線任務)
- 打遊戲🎮(支線任務)
每次你在打遊戲🎮要花不少時間,一局遊戲不打完就算👶哭了也不能中斷,👶非常傷心(我這不爭氣的爹)。
雖然這是一個不恰當的比喻,但是這就是 React 15 的渲染體驗:
- 任務一旦開始,無法中斷
- UI 更新被卡住,用户體驗差
- 無法分配不同優先級的任務
在 Fiber 架構下,帶娃👶和打遊戲🎮這兩件事就變成:
- 時間分片:在帶娃👶的時候如果有空閒時間那麼我就可以打一會遊戲
- 任務可中斷:一旦娃👶哭了,那麼我就立馬暫停手中的遊戲(去帶娃👶)
- 任務可恢復:娃👶不哭了,那麼我又可以重新在暫停的地方開始遊戲🎮啦
- Lane 模型:劃分優先級,帶娃👶這件事情上優先級是 No.1,其他事情先靠邊站🙄
2.2 概念
迴歸正題,Fiber 它有兩個含義:
-
數據結構
- 本質是一個 JavaScript 對象
- 每個組件實例對應一個 Fiber 節點
- 保存組件的類型、props、state、副作用等信息
-
執行單元
- React 把一次渲染工作拆分成很多小的 Fiber 任務(單元)
- 這些單元可以分批執行、中途暫停、恢復、甚至丟棄
2.3 常見屬性及説明
Fiber 節點裏的屬性如下:
| 屬性名 | 類型説明 | 描述 | |
|---|---|---|---|
tag |
number |
節點類型,如 FunctionComponent、ClassComponent 等。 | |
key |
string 或 null |
用於 diff 的唯一標識。 | |
elementType |
任意 |
JSX 轉換後的原始類型,如函數、類、'div' 等。 | |
type |
任意 |
節點對應的組件類型或原始標籤名。 | |
stateNode |
對象或 null |
對於 DOM 節點是真實 DOM,對於 class 是實例,函數組件為 null。 | |
return |
Fiber 或 null |
指向父節點。 | |
child |
Fiber 或 null |
第一個子節點。 | |
sibling |
Fiber 或 null |
下一個兄弟節點。 | |
index |
number |
在兄弟節點中的位置索引。 | |
ref |
ref 對象或 null |
組件 ref。 | |
pendingProps |
任意 |
本次渲染傳入的新 props。 | |
memoizedProps |
任意 |
上一次渲染的 props。 | |
memoizedState |
任意 |
上一次渲染的 state 或 hooks。 | |
updateQueue |
對象或 null |
setState 等更新隊列。 | |
dependencies |
對象或 null |
記錄當前組件使用的 context。 | |
mode |
number |
Fiber 的模式標誌,例如是否啓用 ConcurrentMode。 | |
flags |
位掩碼 |
當前節點本身的副作用標誌。 | |
subtreeFlags |
位掩碼 |
子樹副作用集合。 | |
deletions |
Fiber[] 或 null |
待刪除的子節點列表。 | |
lanes |
位掩碼 |
當前節點的優先級集合。 | |
childLanes |
位掩碼 |
子樹中包含的優先級合集。 | |
alternate |
Fiber 或 null |
另一份 Fiber 節點(workInProgress)。 | |
actualDuration |
number |
Profiler 記錄的渲染耗時。 | |
actualStartTime |
number |
Profiler 渲染開始時間。 | |
selfBaseDuration |
number |
自身渲染耗時。 | |
treeBaseDuration |
number |
子樹渲染總耗時。 | |
_debugOwner |
Fiber 或 null |
DEV 環境用於調試的父組件。 | |
_debugSource |
對象或 null |
DEV 環境的源碼位置信息。 |
2.3 Fiber 樹
Fiber 樹就是以 Fiber 節點為最小單位組織成的樹結構,它用來描述 React 裏的頁面結構:
3 Lane
3.1 概念
Lane 是 React 內部用於調度系統的一種優先級管理機制。每個 Lane 是一個二進制位(bit),多個 Lane 可以通過按位或(|)組合成一個 bitmask,從而實現多任務的並行調度與優先級控制。不同類型的更新任務(如同步更新、過渡動畫、輸入事件等)會被分配到不同的 Lane,React 會根據這些 Lane 的優先級來決定執行順序。
| Lane名稱 | 二進制值 | 十進制值 | 説明 | |
|---|---|---|---|---|
| SyncLane | 0b000...0001 |
1 |
同步更新(最高優先級,如 setState in flushSync) |
|
| InputContinuousLane | 0b000...0010 |
2 |
連續輸入事件(如拖動、滾動) | |
| DefaultLane | 0b000...0100 |
4 |
默認更新優先級 | |
| TransitionLanes | 0b000...1xxx |
8 ~ 0x800000 |
各類 transition,支持併發過渡,如 startTransition | |
| TransitionLane1 | 0b000...1000 |
8 |
第一個 transition lane | |
| TransitionLane2 | 0b000...1_0000 |
16 |
第二個 transition lane | |
| ...(共 16 個) | ... | ... | 一直延續到 TransitionLane16(1 << 21) |
|
| RetryLanes | ... | 1 << 22 ~ 1 << 23 |
Suspense retry 後的更新 | |
| IdleLane | 0b100...0000 |
1073741824 |
最低優先級,後台/預渲染任務 | |
| OffscreenLane | 1 << 29 |
536870912 |
用於隱藏組件的更新(如 <Offscreen />) |
相應的,React 裏也將事件分門別類並且分別賦予了不同的優先級:
| 事件優先級名稱 | 對應的 Lane | 常見對應事件類型(事件名) | 描述 | |
|---|---|---|---|---|
NoEventPriority |
NoLane |
無(無任務) | 沒有任務,空閒狀態 | |
DiscreteEventPriority |
SyncLane |
點擊(click)、鍵盤按鍵(keydown)、提交(submit)等離散事件 | 最高優先級,立即響應用户操作 | |
ContinuousEventPriority |
InputContinuousLane |
鼠標拖拽(mousemove)、滾動(scroll)、連續按鍵(keypress)等連續輸入事件 | 用户持續輸入,保持流暢體驗 | |
DefaultEventPriority |
DefaultLane |
定時器回調、異步請求完成等默認優先級事件 | 默認普通優先級 | |
IdleEventPriority |
IdleLane |
空閒回調、後台任務 | 低優先級,僅在主線程空閒時執行 |
3.2 相關方法
要理解源碼裏關於 Lane 操作的方法,首先就要理解 Lanes 和 Lane 的區別。
本質上 Lane 和 Lanes 都是 number,但是 Lane 用來單獨表示一個優先級,而 Lanes 則用來表示多個優先級。
| 對比項 | Lane |
Lanes |
|
|---|---|---|---|
| 定義 | 單個優先級標識,表示一個更新任務的優先級位。 | 多個優先級的組合,表示當前包含哪些優先級的更新。 | |
| 類型 | number(通常是 2^n,即 0b000...1 形式) |
number(多個 Lane 的按位或運算結果) |
|
| 二進制形式 | 僅一個位為 1,例如:0b000000000000000000000010 |
多個位可以為 1,例如:0b000000000000000000000110 |
|
| 作用 | 表示某一個具體的更新優先級 | 表示當前任務涉及的所有優先級 | |
| 用途舉例 | getHighestPriorityLane(lanes): Lane |
workInProgressRootRenderLanes: Lanes |
|
| 常用操作 | 單獨判斷、與 Lanes 進行比較等 |
位運算(如合併:mergeLanes(a, b),判斷:includesSomeLane(lanes, lane)) |
|
| 使用場景 | 表示當前某個任務的目標優先級 | 表示當前某個 Fiber 或 Root 上所有掛起的任務優先級 | |
| 是否複合類型 | 否(僅代表一個 bit) | 是(可能包含多個 bit) |
3.2.1 getHighestPriorityLane
export function getHighestPriorityLane(lanes: Lanes): Lane {
return lanes & -lanes;
}
這個函數是用來獲取 Lanes 裏最高優先級的 Lane 來優先進行處理。
在二進制運算裏:-lanes = ~lanes + 1,因此 lanes & -lanes 就相當於 lanes & (~lanes + 1),所以我們可以拿到 lanes 最右邊的 1。
在 Lane 的設計裏,優先級是從左到右遞增的,因此最右邊的 Lane 優先級就是最高的。
3.2.2 mergeLanes
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b;
}
儘管 React 裏有優先級控制,但是往往同一個事件會有多個優先級疊加,這時候就需要將優先級提上來了,而 mergeLanes 是用來合併 2 個不同的優先級的。
舉例🌰:對於 001 和 010 這兩個優先級,合併之後就變成了 011。
3.2.3 removeLanes
removeLanes 顧名思義就是將一個子 lanes 從 Lanes 裏移除掉。
export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
return set & ~subset;
}
源碼裏這裏比較好理解,就是直接 set & ~subset,這樣返回的 Lanes 裏就不包含 subset 了。
3.2.4 intersectLanes
intersectLanes 就是取 a 和 b 2 個 Lanes 的交集
export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a & b;
}
因為 Lane 是用二進制位的 1 來表示的,所以説用 & 操作之後,對應二進制位上沒有 1 的部分都會被去掉。
3.2.5 isSubsetOfLanes
判斷目標 Lane 是否為 Lanes 的子集。
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane): boolean {
return (set & subset) === subset;
}
簡單理解:如果 set & subset 的結果是 subset 的話,説明 set 和 subset 的交集是 subset,也就是説 subset 是 set 的子集。
3.2.6 includeSomeLane
判斷目標 Lanes 是否包含 Lane:
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane): boolean {
return (a & b) !== NoLanes;
}
這個函數只判斷是否有交集,和上面的 intersectLanes 方法有些類似,但是返回的是 boolean。
3.2.7 higherPriorityLane
返回優先級更高的 Lane:
export function higherPriorityLane(a: Lane, b: Lane): Lane {
// This works because the bit ranges decrease in priority as you go left.
return a !== NoLane && a < b ? a : b;
}
因為 Lane 是 bitmask 的設計,低位優先級更高,所以這裏判斷優先級的方法是判斷大小,數字越小優先級越高。
3.2.8 pickArbitraryLaneIndex
function pickArbitraryLaneIndex(lanes: Lanes) {
return 31 - clz32(lanes);
}
clz32 是 JS 內置函數,全稱 Count Leading Zeros in 32-bit integer。
它會返回 32 位無符號整數開頭有多少個連續的 0。
因此 pickArbitraryLaneIndex 就是返回對應 Lanes 二進制位裏 1 的最高位,可以理解為拿到 Lanes 裏優先級最低的 Lane 的 index。
3.2.9 getLanesOfEqualOrHigherPriority
獲取一個大於等於目標 Lanes 的 Lane:
function getLanesOfEqualOrHigherPriority(lanes: Lane | Lanes): Lanes {
// Create a mask with all bits to the right or same as the highest bit.
// So if lanes is 0b100, the result would be 0b111.
// If lanes is 0b101, the result would be 0b111.
const lowestPriorityLaneIndex = 31 - clz32(lanes);
return (1 << (lowestPriorityLaneIndex + 1)) - 1;
}
前面提到 31 - clz32(lanes) 其實就是獲取 bitmask 裏最左位的 1 的 index。
因此,如果 lanes 為 0b100,那麼 lowestPriorityLaneIndex + 1 則為 3。
所以 1 << (lowestPriorityLaneIndex + 1) 則為 0b1000,減掉 1 之後返回的值為 0b0111。
這樣一來,我們就可以通過這個函數來拿到“優先級相同或更高”的所有 lanes。
4 總結
在這篇文章裏,我們對 Fiber架構和 Lane模型都有了初步的瞭解,對於後續繼續深入閲讀 React 源碼幫助非常大。