說明#
- 本文基於 v18.1.0 進行分析。
- 在對於 useState 進行調試時會反覆出現 interleaved 這一概念,請參考該 鏈接
- 文章大致上基於下面的代碼進行分析,根據情況不同會有一定的改動。
- 2022-10-14 補充了關於批處理的源碼分析(涉及 任務重用【相關】 與 高優先級插隊【代碼裡有順嘴一提】)與調度階段的簡單總結
import { useState } from "react";
function UseState() {
const [text, setText] = useState(0);
return (
<div onClick={ () => { setText(1) } }>{ text }</div>
)
}
export default UseState;
TLNR#
- useState 的 dispatch 是一個異步操作
- useState 的實現基於 useReducer,或者說 useState 就是一個特殊的 useReducer
- useState 每次調用其 dispatch 都會生成一個對應的 update 實例,多次調用同一個 dispatch 則會掛載成為一個 update 的鏈表。並且每個 update 都擁有一個對應的優先級(lane)
- 如果生成的 update 被判斷為需要被調度更新,則會以 microTask 的形式被調度更新
- 在 useState 的 update 階段內為了快速響應高優先級的 update,update 鏈表只會先處理高優先級的 update,低優先級的 update 會在下一次 render 階段內被調用更新。
- 批處理與(調度 → 協調 → 渲染)內的調度階段總結請移步文末
mount 場景下的 useState#
直接對 useState 打斷點進行調試,我們會進入 mountState 內
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
···
// initialState 為 useState 傳入的初始化值
return mountState(initialState);
···
}
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// hooks 鏈表生成步驟,見[之前的文章](https://github.com/IWSR/react-code-debug/issues/2)
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// initialState 的類型聲明表示它也可以是個函數
initialState = initialState();
}
// hook 上的 memoizedState、baseState 會緩存 useState 的初始值
hook.memoizedState = hook.baseState = initialState;
// 更新時會提到,比較重要的結構
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue; // useState 對應的 hooks 對象會多個 queue 屬性
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any)); // 本文將分析的重點,非常重要的函數
// 對應 const [text, setText] = useState(0); 的返回值
return [hook.memoizedState, dispatch];
}
就這樣 mount 階段的 useState 的分析完了,東西很少。
另外請注意一下 basicStateReducer 這一函數。
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
總結#
mount 階段的 useState 大致起到一個初始化的作用,在結束該函數的調用後我們會獲得該 useState 對應的 hooks 對象。該對象長這樣
觸發 useState 的 dispatch#
此時將斷點打到 useState 返回的 dispatch 上,觸發點擊事件。獲得下面的調用棧
dispatchSetState 是個關鍵的函數,再次強調一下。
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
/**
* 這裡暫時理解為獲取當前 update 的優先級即可
*/
const lane = requestUpdateLane(fiber);
/**
* 與 setState 的 update 鏈表類似
* 這裡的 update 也是一個鏈表結構
*/
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
/**
* 判斷是否是渲染中更新
*/
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
/**
* 判斷 react 當前是否空閒
* 比方說在 第一次調用 dispatch 後,fiber.lanes 就不為 NoLanes
* 因此 第二次調用 dispatch 便不會進入底下對 update 實例內的 eagerState 求值
*/
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
const currentState: S = (queue.lastRenderedState: any);
/**
* 計算期望的 state 並暫存入 eagerState
* lastRenderedReducer 內邏輯如下
* 判斷當前 action 是否是一個函數
* 畢竟也存在 setText((preState) => preState + 1)這樣的調用方式存在
* 如果是函數傳入 currentState 並調用 action 獲得計算後的值
* 如果不是,比如setText(1)這樣的調用方式,就直接返回 action
*/
const eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true; // 根據註釋 防止被 reducer 重複計算
update.eagerState = eagerState;
/**
* 判斷兩者的值是否相等 淺比較 ===
*/
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
/**
* 根據註釋,如果值未改變,則不需要去調度 react 去重新渲染
* 底下這個函數只是去處理 update 鏈表的結構
*/
enqueueConcurrentHookUpdateAndEagerlyBailout(
fiber,
queue,
update,
lane,
);
return; // 因為不需要調度,因此函數在這裡中斷
}
}
}
/**
* 因為運行到這裡的 update 是被認為必須要被重新渲染到頁面上的
* enqueueConcurrentHookUpdate 與 enqueueConcurrentHookUpdateAndEagerlyBailout 不同
* 會去調用 markUpdateLaneFromFiberToRoot
* 該函數會將當前 update 對應的 lane 自發生更新的 fiber 節點開始一層一層向上遞歸(fiber.return)
* 標記到其父 fiber 的 childLanes 上(比方說fiber.return -> fiber.return.return),直到 root 為止
* 調用 markUpdateLaneFromFiberToRoot 的目的是為 scheduleUpdateOnFiber 的調度做準備的
*/
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
/**
* 進入調度流程(scheduleUpdateOnFiber 的分析已經更新在最下面了)
* 不過在 v18 中會被 queueMicrotask 註冊為 microTask
* 在 v17 則是以對應的優先級註冊進 scheduler
*/
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
/**
* 一個不穩定的函數,大致上是處理 lanes,有點難懂不解析了
*/
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane, action);
}
總結一下 useState 的 dispatch#
對於 useState 的 dispatch 來說,每調用一次 dispatch 都會生成一個新的 update 實例且該實例會被掛載到其 hooks 的鏈表,但是調用 dispatch 後生成的新值會與其原始值(currentState)經過一次淺比較來決定這一次的更新是否需要被調度更新。在 v18 的版本中,這一次的調度任務是存在於 microTask 中的,因此可認為 useState 的 dispatch 屬於異步操作。下面簡單寫個例子驗證下。
import { useState } from "react";
function UseState() {
const [text, setText] = useState(0);
return (
<div onClick={ () => { setText(1); console.log(text); } }>{ text }</div>
)
}
export default UseState;
點擊後得到的結果
其調用棧,不難看到 microtask。
update 場景下的 useState#
接下來我們稍微在原來的調試代碼上添加一個 dispatch,新的調試代碼大概長這樣。
import { useEffect, useState } from "react";
function UseState() {
const [text, setText] = useState(0);
return (
<div onClick={ () => {
setText(1);
setText(2); // 給這裡打個斷點
} }>{ text }</div>
)
}
export default UseState;
對整個頁面的表現做一個記錄,大致長這樣
順帶一提
第二次 dispatch 時因為會不滿足 fiber.lanes === NoLanes 這個條件因此直接跳入 enqueueConcurrentHookUpdate (具體看一下 dispatchSetState 的代碼)
而在 enqueueConcurrentHookUpdate 對 update 鏈表連接後我們會得到如下結構的環状鏈表
回到正題,在 useState 的 update 階段中會調用 updateState 內的 updateReducer 這一函數(就是 uesReducer 對應的 mount 版本)
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
/**
* 這個函數在 use(Layout)Effect 內也出現過,大致作用是
* 同時移動 WIP hooks 和 current hooks 的指針向後移動一位
* 詳細註釋可看 [React Hooks: 附錄](https://github.com/IWSR/react-code-debug/issues/4)
*/
const hook = updateWorkInProgressHook(); // WIP hooks對象
const queue = hook.queue; // update 鏈表
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
// 在 useState 的場景中 lastRenderedReducer 為 basicStateReducer —— 在 mountState 中預設的 reducer
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
// 獲取 pending 狀態的 update,還未被計算
const pendingQueue = queue.pending;
/**
* 整理 update queue,為後續的更新做準備
*/
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
// 將 pendingQueue 與 baseQueue 相連
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
/**
* 處理後的 baseQueue 包含了所有 update 實例
*/
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
/**
* 篩選出被包含在 renderLanes 內的 update
* 這步驟和位操作有關
* updateLane 如果屬於 renderLanes 內,則代表著當前 update 的優先級比較緊急
* 需要被處理,反之則可以跳過,
* 類似的邏輯也存在於 class 組件的 update 鏈表內(這裡不詳細展開)
* 不過這裡是 ! 操作,所以都是反的
*/
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
/**
* 進入了這個邏輯的 update 便意味著不是很緊急,可以慢慢處理
* 但是這並不意味著不處理,在空閒時仍然會從被跳過的 update 開始重新再計算一次狀態
* 因此需要緩存被跳過的 update 實例
*/
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
/**
* 將跳過的 update 添加到 newBaseQueue 中
* 等到下一次 render 再重新計算
*/
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
// 更新 WIP fiber 的 lanes,下一次 render 時重新執行跳過 update 的關鍵
// completeWork 時會將這些 lanes 收集到 root(合併到 root 發生在 commitRootImpl),然後再重新調度(commitRootImpl 內的 ensureRootIsScheduled)
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
// 標記跳過的 lanes 到 workInProgressRootSkippedLanes 上
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.
// 優先級足夠的場景,便執行更新
if (newBaseQueueLast !== null) {
/**
* newBaseQueueLast 不為空,說明存在被跳過的 update
* update 的狀態計算可能存在聯繫
* 因此一旦有update被跳過,就以它為起點,
* 將後邊直到最後的 update 無論優先級如何都截取下來。
* (和 class 組件的處理邏輯差不多)
*/
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Process this update.
// 執行本次的 update,計算新的 state
if (update.hasEagerState) {
// 已經計算過的狀態會被打上標記,避免重複計算
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
// 根據 state 和 action 計算新的 state
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// Mark that the fiber performed work, but only if the new state is
// different from the current state.
// === 校驗不通過,則在當前的 WIP fiber 上標記更新,在 completeWork 階段中會被收集到 root 上
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 更新新的數據到 hook 內
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
// Interleaved updates are stored on a separate queue. We aren't going to
// process them during this render, but we do need to track which lanes
// are remaining.
/**
* 處理 interleaved update
* 但我決定跳過這一塊的分析,因為我完全不知道什麼樣的更新才能被叫做 interleaved update
* 在 /react-reconciler/src/ReactFiberWorkLoop.old.js 內有著這一塊的描述
* Received an update to a tree that's in the middle of rendering. Mark
that there was an interleaved update work on this root.
* 但是又和 enqueueConcurrentHookUpdate 與
* enqueueConcurrentHookUpdateAndEagerlyBailout 內對於 queue.interleaved 的設置完全搭不上邊
* 如果有對這方面有了解的朋友也請麻煩指導一下
*/
const lastInterleaved = queue.interleaved;
if (lastInterleaved !== null) {
let interleaved = lastInterleaved;
do {
const interleavedLane = interleaved.lane;
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
interleavedLane,
);
markSkippedUpdateLanes(interleavedLane);
interleaved = ((interleaved: any).next: Update<S, A>);
} while (interleaved !== lastInterleaved);
} else if (baseQueue === null) {
// `queue.lanes` is used for entangling transitions. We can set it back to
// zero once the queue is empty.
queue.lanes = NoLanes;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
總結一下#
useState 的 update 階段會通過 dispatch 函數生成的 update 鏈表去更新對應的狀態,然而並非所有的 update 都會被更新,只有其優先級(lane)屬於當前 renderLanes 內才會被優先計算,而跳過的 update 會被標記並在下一次 render 時被更新。
簡單提供個例子來證明
import { useEffect, useState, useRef, useCallback, useTransition } from "react";
function UseState() {
const dom = useRef(null);
const [number, setNumber] = useState(0);
const [, startTransition] = useTransition();
useEffect(() => {
const timeout1 = setTimeout(() => {
startTransition(() => { // 將其優先級降級,從而被 timeout2 內的 update 插隊
setNumber((preNumber) => preNumber + 1);
});
}, 500 )
const timeout2 = setTimeout(() => {
dom.current.click();
}, 505)
return () => {
clearTimeout(timeout1);
clearTimeout(timeout2);
}
}, []);
const clickHandle = useCallback(() => {
console.log('click');
setNumber(preNumber => preNumber + 2);
}, []);
return (
<div ref={dom} onClick={ clickHandle }>
{
Array.from(new Array(20000)).map((item, index) => <span key={index}>{ number }</span>)
}
</div>
)
}
export default UseState;
從運行結果來看,由於 timeout1 的 update 被 startTransition 降級,因此 timeout2 的 update 被優先更新了。
批處理#
批處理其實在 v17 時代便已經有了,不過 v18 把它的限制條件給去除了然後就成了自動批處理(惱)。具體可以看看這篇文章。
比方說我們這麼一段代碼在運行(當然這裡的調試環境依然是 v18)
import { useEffect, useState } from "react";
function UseState() {
const [num1, setNum1] = useState(1);
const [num2, setNum2] = useState(2);
useEffect(() => {
setNum1(11);
setNum2(22);
}, []);
return (
<>
<div>{ num1 }</div>
<div>{ num2 }</div>
</>
)
}
根據上文中對 dispatchSetState 的分析中其實有提到,只要是有必要更新到頁面的 update 就會調用 scheduleUpdateOnFiber 來觸發渲染。那么例子中連續調用兩次 dispatch ,也就是說觸發了兩次 scheduleUpdateOnFiber,那麼問題來了 —— 頁面渲染了幾次呢?
就一次。
至於為什麼需要去看 scheduleUpdateOnFiber 裡面做了什麼,scheduleUpdateOnFiber 涉及到了整個更新的入口,因此也是一個比較重要的函數。
該函數主要負責處理
- 檢查是否存在無限更新 —— checkForNestedUpdates
- 在 root.pendingLanes 上標記存在需要更新的 update 的 lanes —— markRootUpdated
- 觸發 ensureRootIsScheduled,進入任務調度的核心函數
scheduleUpdateOnFiber#
export function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
Count the number of times the root synchronously re-renders without
// finishing. If there are too many, it indicates an infinite update loop.
// 檢查是否存在無限更新,主要通過設置了一個上限 —— NESTED_UPDATE_LIMIT(50)
checkForNestedUpdates();
// Mark that the root has a pending update.
// 在 root.pendingLanes 上標記存在需要更新的 update 的 lanes
markRootUpdated(root, lane, eventTime);
if (
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
/**
* 錯誤處理,跳過
*/
} else {
...
/**
* 重要函數!觸發任務調度的核心
* 我們要找的邏輯也在裡面
*/
ensureRootIsScheduled(root, eventTime);
...
// 底下是 Legacy 模式的兼容代碼,不看了
}
}
這時我們又遇到了一个 react 內的重要函數 —— ensureRootIsScheduled,進入該函數就能看到任務調度在 react 內的核心邏輯了(還有一部分核心邏輯在 Scheduler 內,可以看我寫的 React Scheduler: Scheduler 源碼分析)
ensureRootIsScheduled#
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
/**
* root.callbackNode 是 Scheduler 在執行調度時生成的任務,
* 而這個值會在 react 調用 scheduleCallback 時給 return 出來並賦值給 root.callbackNode
* 而從 existingCallbackNode 的變量名上不難推測,這個變量用以表示已經存在的被調度的任務(舊任務)
*/
const existingCallbackNode = root.callbackNode;
// Check if any lanes are being starved by other work. If so, mark them as
// expired so we know to work on those next.
/**
* 這裡是為了檢查正在等待的任務中是否存在已經過期了的任務(比方說一些高優先級的任務一直插隊,
* 導致低優先級的任務沒法被執行而過期了),如果存在過期了的任務,則把他們的 lane標記到
* expiredLanes 上,方便 react 以同步優先級立刻調用他們
*/
markStarvedLanesAsExpired(root, currentTime);
// Determine the next lanes to work on, and their priority.
// 獲取 renderLanes
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
/**
* 如果 renderLanes 為空,那麼意味當前不需要啟動調度,跳出
*/
if (nextLanes === NoLanes) {
// Special case: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// We use the highest priority lane to represent the priority of the callback.
/**
* 生成 renderLanes 內最優先的任務對應的 優先級
*/
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// Check if there's an existing task. We may be able to reuse it.
/**
* 獲取舊任務的優先級
*/
const existingCallbackPriority = root.callbackPriority;
/**
* 如果兩個任務的優先級相同,那麼直接跳出,因為已經存在了對應的任務可以復用
* 這裡也是實現 批處理 的地方
*/
if (
existingCallbackPriority === newCallbackPriority
) {
// The priority hasn't changed. We can reuse the existing task. Exit.
// 優先級未改變,因此可以復用舊任務,於是退出
return;
}
/**
* 插隊邏輯
* 進入這裡的邏輯都可以認為其優先級比舊任務的優先級要高,於是需要重新調度
* 於是舊任務就沒有被調用的必要了,於是可以取消了。(取消邏輯可以看 Scheduler 內的邏輯)
*/
if (existingCallbackNode != null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}
// 調度一個新任務
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
/**
* 同步優先級意味著這可能是過期任務或是當前的模式為非 concurrent 模式
*/
if (root.tag === LegacyRoot) {
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
if (supportsMicrotasks) {
// Flush the queue in a microtask.
/**
* 要是瀏覽器支持 microtask,就把 performSyncWorkOnRoot 丟到 microtask 內
* 這樣子就可以更快的執行
* 一個 eventLoop 遵循 一個old MacroTask ——> 清空microtasks ——> 瀏覽器渲染頁面 這樣的順序執行
* 不然進入 Scheduler 內,MessageChannel 處理的任務可都是 MacroTask,那就會在下個
* eventLoop 內再渲染了
*/
scheduleMicrotask(() => {
if (
(executionContext & (RenderContext | CommitContext)) ===
NoContext
) {
/**
* 這裡會把上面調度的任務取消掉然後再調用 performSyncWorkOnRoot
*/
flushSyncCallbacks();
}
});
} else {
// Flush the queue in an Immediate task.
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
}
newCallbackNode = null;
} else {
// concurrent模式的處理邏輯
let schedulerPriorityLevel;
// 把 renderLanes 轉成 調度優先級
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
/**
* 用對應的優先級讓 Scheduler 調度 react的任務了
*/
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
// 更新root上的任務優先級和任務,以便下次發起調度時可以獲取到
// 就開頭用到的 oldTask
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
分析完 ensureRootIsScheduled 我們也對批處理是如何實現的有了大致的了解,它的實現其實就是對 react 內同優先級任務的復用,這樣子就做到了多次觸發 hooks 的 dispatch 但是只渲染了一次。
不過到這配合上 React Scheduler: Scheduler 源碼分析 其實我們已經把 react 內三大模塊(調度 → 協調 → 渲染),調度的部分全部介紹完了,接下來讓我一個問題來把整篇文章串起來。
React 從觸發 useState 的 dispatch 到渲染到頁面做了哪些事 —— 調度篇
- 生成一個對應優先級的 update 實例,並以循環鏈表的形式掛載到 useState 對應的 hooks 實例上的 queue 屬性上
- 如果當前 update 被判斷為需要被渲染到頁面上的,則會將這個 update 上的 lane 自當前 fiber 節點向其父節點的 childLanes 上層層標記,直到 root 為止
- 隨後會調用 scheduleUpdateOnFiber 進入調度階段
- 首先將當前 update 的 lanes 標記到 root.pendingLanes 上,pendingLanes 上的任務可以視為所有待更新的任務
- 隨後取 pendingLanes 內最高的優先級作為當前的 renderLanes
- 在使用 Scheduler 註冊調度任務前,會先檢查是否存在已註冊但還未執行的任務(root.callbackNode),如果存在任務便用該任務的任務優先級與 renderLanes 比較
- 任務優先級相同,則復用該任務;
- 如果 renderLanes 更高則將舊任務(root.callbackNode)取消重新在 Scheduler 內註冊任務,並將返回的任務賦值給 root.callbackNode