React中的任務(wù)饑餓行為
本文是在React中的高優(yōu)先級(jí)任務(wù)插隊(duì)機(jī)制基礎(chǔ)上的后續(xù)延伸,先通過閱讀這篇文章了解任務(wù)調(diào)度執(zhí)行的整體流程,有助于更快地理解本文所講的內(nèi)容。
饑餓問題說到底就是高優(yōu)先級(jí)任務(wù)不能毫無底線地打斷低優(yōu)先級(jí)任務(wù),一旦低優(yōu)先級(jí)任務(wù)過期了,那么他就會(huì)被提升到同步優(yōu)先級(jí)去立即執(zhí)行。如下面的例子:
我點(diǎn)擊左面的開始按鈕,開始渲染大量DOM節(jié)點(diǎn),完成一次正常的高優(yōu)先級(jí)插隊(duì)任務(wù):
而一旦左側(cè)更新的時(shí)候去拖動(dòng)右側(cè)的元素,并在拖動(dòng)事件中調(diào)用setState記錄坐標(biāo),介入更高優(yōu)先級(jí)的任務(wù),這個(gè)時(shí)候,左側(cè)的DOM更新過程會(huì)被暫停,不過當(dāng)我拖動(dòng)到一定時(shí)間的時(shí)候,左側(cè)的任務(wù)過期了,那它就會(huì)提升到同步優(yōu)先級(jí)去立即調(diào)度,完成DOM的更新(低優(yōu)先級(jí)任務(wù)的lane優(yōu)先級(jí)并沒有變,只是任務(wù)優(yōu)先級(jí)提高了)。
要做到這樣,React就必須用一個(gè)數(shù)據(jù)結(jié)構(gòu)去存儲(chǔ)pendingLanes中有效的lane它對(duì)應(yīng)的過期時(shí)間。另外,還要不斷地檢查這個(gè)lane是否過期。
這就涉及到了任務(wù)過期時(shí)間的記錄 以及 過期任務(wù)的檢查。
lane模型過期時(shí)間的數(shù)據(jù)結(jié)構(gòu)
完整的pendingLanes有31個(gè)二進(jìn)制位,為了方便舉例,我們縮減位數(shù),但道理一樣。
例如現(xiàn)在有一個(gè)lanes:
- 0 b 0 0 1 1 0 0 0
那么它對(duì)應(yīng)的過期時(shí)間的數(shù)據(jù)結(jié)構(gòu)就是這樣一個(gè)數(shù)組:
- [ -1, -1, 4395.2254, 3586.2245, -1, -1, -1 ]
在React過期時(shí)間的機(jī)制中,-1 為 NoTimestamp
即pendingLanes中每一個(gè)1的位對(duì)應(yīng)過期時(shí)間數(shù)組中一個(gè)有意義的時(shí)間,過期時(shí)間數(shù)組會(huì)被存到root.expirationTimes字段。這個(gè)計(jì)算和存取以及判斷是否過期的邏輯
是在markStarvedLanesAsExpired函數(shù)中,每次有任務(wù)要被調(diào)度的時(shí)候都會(huì)調(diào)用一次。
記錄并檢查任務(wù)過期時(shí)間
在React中的高優(yōu)先級(jí)任務(wù)插隊(duì)機(jī)制那篇文章中提到過,ensureRootIsScheduled函數(shù)作為統(tǒng)一協(xié)調(diào)任務(wù)調(diào)度的角色,它會(huì)調(diào)用markStarvedLanesAsExpired函數(shù),目的是把當(dāng)前進(jìn)來的這個(gè)任務(wù)的過期時(shí)間記錄到root.expirationTimes,并檢查這個(gè)任務(wù)是否已經(jīng)過期,若過期則將它的lane放到root.expiredLanes中。
- function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
- // 獲取舊任務(wù)
- const existingCallbackNode = root.callbackNode;
- // 記錄任務(wù)的過期時(shí)間,檢查是否有過期任務(wù),有則立即將它放到root.expiredLanes,
- // 便于接下來將這個(gè)任務(wù)以同步模式立即調(diào)度
- markStarvedLanesAsExpired(root, currentTime);
- ...
- }
markStarvedLanesAsExpired函數(shù)的實(shí)現(xiàn)如下:
暫時(shí)不需要關(guān)注suspendedLanes和pingedLanes
- export function markStarvedLanesAsExpired(
- root: FiberRoot,
- currentTime: number,
- ): void {
- // 獲取root.pendingLanes
- const pendingLanes = root.pendingLanes;
- // suspense相關(guān)
- const suspendedLanes = root.suspendedLanes;
- // suspense的任務(wù)被恢復(fù)的lanes
- const pingedLanes = root.pingedLanes;
- // 獲取root上已有的過期時(shí)間
- const expirationTimes = root.expirationTimes;
- // 遍歷待處理的lanes,檢查是否到了過期時(shí)間,如果過期,
- // 這個(gè)更新被視為饑餓狀態(tài),并把它的lane放到expiredLanes
- let lanes = pendingLanes;
- while (lanes > 0) {
- /*
- pickArbitraryLaneIndex是找到lanes中最靠左的那個(gè)1在lanes中的index
- 也就是獲取到當(dāng)前這個(gè)lane在expirationTimes中對(duì)應(yīng)的index
- 比如 0b0010,得出的index就是2,就可以去expirationTimes中獲取index為2
- 位置上的過期時(shí)間
- */
- const index = pickArbitraryLaneIndex(lanes);
- const lane = 1 << index;
- // 上邊兩行的計(jì)算過程舉例如下:
- // lanes = 0b0000000000000000000000000011100
- // index = 4
- // 1 = 0b0000000000000000000000000000001
- // 1 << 4 = 0b0000000000000000000000000001000
- // lane = 0b0000000000000000000000000001000
- const expirationTime = expirationTimes[index];
- if (expirationTime === NoTimestamp) {
- // Found a pending lane with no expiration time. If it's not suspended, or
- // if it's pinged, assume it's CPU-bound. Compute a new expiration time
- // using the current time.
- // 發(fā)現(xiàn)一個(gè)沒有過期時(shí)間并且待處理的lane,如果它沒被掛起,
- // 或者被觸發(fā)了,那么去計(jì)算過期時(shí)間
- if (
- (lane & suspendedLanes) === NoLanes ||
- (lane & pingedLanes) !== NoLanes
- ) {
- expirationTimes[index] = computeExpirationTime(lane, currentTime);
- }
- } else if (expirationTime <= currentTime) {
- // This lane expired
- // 已經(jīng)過期,將lane并入到expiredLanes中,實(shí)現(xiàn)了將lanes標(biāo)記為過期
- root.expiredLanes |= lane;
- }
- // 將lane從lanes中刪除,每循環(huán)一次刪除一個(gè),直到lanes清空成0,結(jié)束循環(huán)
- lanes &= ~lane;
- }
- }
通過markStarvedLanesAsExpired的標(biāo)記,過期任務(wù)得以被放到root.expiredLanes中在隨后獲取任務(wù)優(yōu)先級(jí)時(shí),會(huì)優(yōu)先從root.expiredLanes中取值去計(jì)算優(yōu)先級(jí),這時(shí)得出的優(yōu)先級(jí)是同步級(jí)別,因此走到下面會(huì)以同步優(yōu)先級(jí)調(diào)度。實(shí)現(xiàn)過期任務(wù)被立即執(zhí)行。
- function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
- // 獲取舊任務(wù)
- const existingCallbackNode = root.callbackNode;
- // 記錄任務(wù)的過期時(shí)間,檢查是否有過期任務(wù),有則立即將它放到root.expiredLanes,
- // 便于接下來將這個(gè)任務(wù)以同步模式立即調(diào)度
- markStarvedLanesAsExpired(root, currentTime);
- ...
- // 若有任務(wù)過期,這里獲取到的會(huì)是同步優(yōu)先級(jí)
- const newCallbackPriority = returnNextLanesPriority();
- ...
- // 調(diào)度一個(gè)新任務(wù)
- let newCallbackNode;
- if (newCallbackPriority === SyncLanePriority) {
- // 過期任務(wù)以同步優(yōu)先級(jí)被調(diào)度
- newCallbackNode = scheduleSyncCallback(
- performSyncWorkOnRoot.bind(null, root),
- );
- }
- }
記錄并檢查任務(wù)是否過期
concurrent模式下的任務(wù)執(zhí)行會(huì)有時(shí)間片的體現(xiàn),檢查并記錄任務(wù)是否過期就發(fā)生在每個(gè)時(shí)間片結(jié)束交還主線程的時(shí)候??梢岳斫獬稍谡麄€(gè)(高優(yōu)先級(jí))任務(wù)的執(zhí)行期間,
持續(xù)調(diào)用ensureRootIsScheduled去做這件事,這樣一旦發(fā)現(xiàn)有過期任務(wù),可以立馬調(diào)度。
執(zhí)行任務(wù)的函數(shù)是performConcurrentWorkOnRoot,一旦因?yàn)闀r(shí)間片中斷了任務(wù),就會(huì)調(diào)用ensureRootIsScheduled。
- function performConcurrentWorkOnRoot(root) {
- ...
- // 去執(zhí)行更新任務(wù)的工作循環(huán),一旦超出時(shí)間片,則會(huì)退出renderRootConcurrent
- // 去執(zhí)行下面的邏輯
- let exitStatus = renderRootConcurrent(root, lanes);
- ...
- // 調(diào)用ensureRootIsScheduled去檢查有無過期任務(wù),是否需要調(diào)度過期任務(wù)
- ensureRootIsScheduled(root, now());
- // 更新任務(wù)未完成,return自己,方便Scheduler判斷任務(wù)完成狀態(tài)
- if (root.callbackNode === originalCallbackNode) {
- return performConcurrentWorkOnRoot.bind(null, root);
- }
- // 否則retutn null,表示任務(wù)已經(jīng)完成,通知Scheduler停止調(diào)度
- return null;
- }
performConcurrentWorkOnRoot是被Scheduler持續(xù)執(zhí)行的,這與Scheduler的原理相關(guān),可以移步到我寫的一篇長文幫你徹底搞懂React的調(diào)度機(jī)制原理這篇文章去了解一下,如果暫時(shí)不了解也沒關(guān)系,你只需要知道它會(huì)被Scheduler在每一個(gè)時(shí)間片內(nèi)都調(diào)用一次即可。
一旦時(shí)間片中斷了任務(wù),那么就會(huì)走到下面調(diào)用ensureRootIsScheduled。我們可以追問一下時(shí)間片下的fiber樹構(gòu)建機(jī)制,更深入的理解ensureRootIsScheduled
為什么會(huì)在時(shí)間片結(jié)束的時(shí)候調(diào)用。
這一切都要從renderRootConcurrent函數(shù)說起:
- function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
- // workLoopConcurrent中判斷超出時(shí)間片了,
- // 那workLoopConcurrent就會(huì)從調(diào)用棧彈出,
- // 走到下面的break,終止循環(huán)
- // 然后走到循環(huán)下面的代碼
- // 就說明是被時(shí)間片打斷任務(wù)了,或者fiber樹直接構(gòu)建完了
- // 依據(jù)情況return不同的status
- do {
- try {
- workLoopConcurrent();
- break;
- } catch (thrownValue) {
- handleError(root, thrownValue);
- }
- } while (true);
- if (workInProgress !== null) {
- // workInProgress 不為null,說明是被時(shí)間片打斷的
- // return RootIncomplete說明還沒完成任務(wù)
- return RootIncomplete;
- } else {
- // 否則說明任務(wù)完成了
renderRootConcurrent中寫了一個(gè)do...while(true)的循環(huán),目的是如果任務(wù)執(zhí)行的時(shí)間未超出時(shí)間片限制(一般未5ms),那就一直執(zhí)行,
直到workLoopConcurrent調(diào)用完成出棧,brake掉循環(huán)。
workLoopConcurrent中依據(jù)時(shí)間片去深度優(yōu)先構(gòu)建fiber樹
- function workLoopConcurrent() {
- // 調(diào)用shouldYield判斷如果超出時(shí)間片限制,那么結(jié)束循環(huán)
- while (workInProgress !== null && !shouldYield()) {
- performUnitOfWork(workInProgress);
- }
- }
所以整個(gè)持續(xù)檢查過期任務(wù)過程是:一個(gè)更新任務(wù)被調(diào)度,Scheduler調(diào)用performConcurrentWorkOnRoot去執(zhí)行任務(wù),后面的步驟:
- performConcurrentWorkOnRoot調(diào)用renderRootConcurrent,renderRootConcurrent去調(diào)用workLoopConcurrent執(zhí)行fiber的構(gòu)建任務(wù),也就是update引起的更新任務(wù)。
- 當(dāng)執(zhí)行時(shí)間超出時(shí)間片限制之后,首先workLoopConcurrent會(huì)彈出調(diào)用棧,然后renderRootConcurrent中的do...while(true)被break掉,使得它也彈出調(diào)用棧,因此回到performConcurrentWorkOnRoot中。
- performConcurrentWorkOnRoot繼續(xù)往下執(zhí)行,調(diào)用ensureRootIsScheduled檢查有無過期任務(wù)需要被調(diào)度。
- 本次時(shí)間片跳出后的邏輯完成,Scheduler會(huì)再次調(diào)用performConcurrentWorkOnRoot執(zhí)行任務(wù),重復(fù)1到3的過程,也就實(shí)現(xiàn)了持續(xù)檢查過期任務(wù)。
總結(jié)
低優(yōu)先級(jí)任務(wù)的饑餓問題其實(shí)本質(zhì)上還是高優(yōu)先級(jí)任務(wù)插隊(duì),但是低優(yōu)先級(jí)任務(wù)在被長時(shí)間的打斷之后,它的優(yōu)先級(jí)并沒有提高,提高的根本原因是markStarvedLanesAsExpired
將過期任務(wù)的優(yōu)先級(jí)放入root.expiredLanes,之后優(yōu)先從expiredLanes獲取任務(wù)優(yōu)先級(jí)以及渲染優(yōu)先級(jí),即使pendingLanes中有更高優(yōu)先級(jí)的任務(wù),但也無法從pendingLanes中
獲取到高優(yōu)任務(wù)對(duì)應(yīng)的任務(wù)優(yōu)先級(jí)。