Scheduler是React中相對獨立的模塊,用於實現相似瀏覽器requestIdleCallback的功能。也是React16實現time slicing
最核心的一個模塊。node
在我閱讀React源碼的過程來看,scheduler
就是整個React源碼的縮影,例如鏈表的操做、全局變量、臨時變量的控制等等。若是能先弄懂,對其餘部分的代碼閱讀將會有極大的幫助。 我寫這篇文章但願將scheduler
模塊從React源碼中抽離出來解讀,不須要任何其餘部分的知識,將這個模塊做爲閱讀源碼的突破口,在此我須要一些預備知識: 咱們先看一下整個scheduler
的方法列表:react
unstable_scheduleCallback
,咱們接下來說解都會從unstable_scheduleCallback
這個函數開始。其中傳入的參數callback是performAsyncWork
,咱們這邊先無論,直接簡單的理解爲是React要執行的一個任務,由於整個scheduler模塊就是控制這個任務的運行時間。在目前React最新的代碼中expirationTime中,expirationTime越小反而優先級越高,主要是爲了減小sync判斷,可是這個邏輯尚未修改到scheduler模塊中,關於expirationTime還有不少內容,可是這裏並非咱們的重點,這裏先不講。git
firstCallbackNode
,當咱們遇到一個判斷firstCallbackNode === null
,咱們應該明白這是這判斷這個鏈表是否爲空。在後文中,這個鏈表的元素我稱之爲callbackNode
, 鏈表稱爲callback鏈表
接下面咱們從入口函數unstable_scheduleCallback
開始看, unstable_scheduleCallback
函數的內容概述就是生成callbackNode
,並插入到callback鏈表之中github
function unstable_scheduleCallback(callback, deprecated_options) {
// callback是performAsyncWork,也就是咱們上文所說的任務
// getCurrentTime就是獲取當前時間
// currentEventStartTime在react-dom中不會用到,先忽略
var startTime =
currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
var expirationTime;
if (
// 這個判斷與scheduler核心無關,先忽略
typeof deprecated_options === 'object' &&
deprecated_options !== null &&
typeof deprecated_options.timeout === 'number'
) {
// 從requestWork調用到這裏,目前只會走這個分支
// 目前來看timeout越小,優先級越大
expirationTime = startTime + deprecated_options.timeout;
} else {
// 這一部份內容我在React源碼中並無看到用的地方
// 應該是還未完成的一部分代碼
// 這裏先刪除以避免影響閱讀
}
// 這個就是callback環形鏈表的元素結構
var newNode = {
callback, // 須要執行的任務
priorityLevel: currentPriorityLevel, // 這個值暫時用不到,先不看
expirationTime, // 過時時間,上文已經介紹過
next: null, // 鏈表結構next
previous: null, // 鏈表結構previous
};
// 接下來部分就是將newNode插入到鏈表中,而且按expirationTime從大到小的順序
// firstCallbackNode 是一個雙向循環鏈表的頭部,這個鏈表在此模塊(scheduler)模塊維護
// firstCallbackNode === null 說明鏈表爲空
if (firstCallbackNode === null) {
// 給環形鏈表添加第一個元素
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled();
} else {
var next = null;
var node = firstCallbackNode;
// 從頭部(firstCallbackNode)開始遍歷鏈表,知道
do {
// 這個判斷用於尋找expirationTime最大的任務並賦值給next
// 也就是優先級最低的任務
if (node.expirationTime > expirationTime) {
next = node; // next這個局部變量就是爲了從鏈表中找出比當前新進入的callback優先級更小的任務
break;
}
node = node.next;
} while (node !== firstCallbackNode); // 因爲是環形鏈表,這是已經遍歷一圈的標記
// 這裏環形鏈表的排序是這樣的
/* * head * next7 next1 * next6 next2 * next5 next3 * next4 * * 其中head的expirationTime最小,next7最大,其他的next的expirationTime從小到大排序, * 當next === null,走分支1,newNode的expirationTime是最大的(鏈表每一個element都小於newNode),因此須要將newNode插入head以前 * 當next === firstCallbackNode,newNode的expirationTime是最小的,也就是newNode要插入head以前,成爲新的head, * 因此分支2須要修改鏈表的head指針 * */
if (next === null) {
// 分支1
// No callback with a later expiration was found, which means the new
// callback has the latest expiration in the list.
next = firstCallbackNode;
} else if (next === firstCallbackNode) {
// 分支2
// 這個分支是指新的callback的expirationTime最小,那麼應該放在頭部,這裏直接改變頭部(firstCallbackNode)指向newNode
// 後面插入操做正常執行,與上面的判斷分支相似
// The new callback has the earliest expiration in the entire list.
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled();
}
// 環形雙向鏈表插入的常規操做,這裏是指在next節點以前插入newNode
var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}
return newNode;
}
複製代碼
至此咱們明白了,這個函數的功能就是按照expirationTime從小到大排列callback鏈表。只要插入和排序一完成,咱們就會調用ensureHostCallbackIsScheduled
。瀏覽器
function ensureHostCallbackIsScheduled() {
// 當某個callback已經被調用
if (isExecutingCallback) {
// Don't schedule work yet; wait until the next time we yield.
return;
}
// Schedule the host callback using the earliest expiration in the list.
var expirationTime = firstCallbackNode.expirationTime;
if (!isHostCallbackScheduled) {
isHostCallbackScheduled = true;
} else {
// Cancel the existing host callback.
cancelHostCallback();
}
requestHostCallback(flushWork, expirationTime);
}
複製代碼
ensureHostCallbackIsScheduled
在後面會在各類狀況再次調用,這裏咱們只要知道,ensureHostCallbackIsScheduled
,而且調用了requestHostCallback(flushWork, expirationTime)
就能夠了。bash
requestHostCallback = function(callback, absoluteTimeout) {
// scheduledHostCallback就是flushWork
scheduledHostCallback = callback;
// timeoutTime就是callback鏈表的頭部的expirationTime
timeoutTime = absoluteTimeout;
// rAF是requestAnimationFrame的縮寫
// isFlushingHostCallback這個判斷是一個Eagerly操做,若是有新的任務進來,
// 儘可能讓其直接執行,防止瀏覽器在下一幀才執行這個callback
// 這個判斷其實不是很好理解,建議熟悉模塊以後再回來看,並不影響scheduler核心邏輯
// 有興趣能夠閱讀https://github.com/facebook/react/pull/13785
if (isFlushingHostCallback || absoluteTimeout < 0) {
// absoluteTimeout < 0說明任務超時了,馬上執行,不要等下一幀
// Don't wait for the next frame. Continue working ASAP, in a new event.
// port就是port1
port.postMessage(undefined);
// isAnimationFrameScheduled是指animationTick函數是否在運行中
// 第一次調用必定會走進這個分支
} else if (!isAnimationFrameScheduled) {
// If rAF didn't already schedule one, we need to schedule a frame.
// TODO: If this rAF doesn't materialize because the browser throttles, we
// might want to still have setTimeout trigger rIC as a backup to ensure
// that we keep performing work.
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
};
複製代碼
咱們注意到函數名也有callback
,可是這裏是hostCallback
,整個模塊中的hostCallback
函數都是指flushWork
,咱們後面再講這個flushWork
。 註釋中有一個疑點就是判斷isAnimationFrameScheduled
,這裏是由於整個scheduler模塊都是在animationTick
函數中一幀一幀的調用的,咱們在下一個animationTick
函數中會詳細講解。併發
var requestAnimationFrameWithTimeout = function(callback) {
// callback就是animationTick方法
// schedule rAF and also a setTimeout
// localRequestAnimationFrame至關於window.requestAnimationFrame
// 接下來兩個調用時超時併發處理
// 1. 調用requestAnimationFrame
rAFID = localRequestAnimationFrame(function(timestamp) {
// cancel the setTimeout
localClearTimeout(rAFTimeoutID);
callback(timestamp);
});
// 2. 調用setTimeout,時間爲ANIMATION_FRAME_TIMEOUT(100),超時則取消rAF,改成直接調用
rAFTimeoutID = localSetTimeout(function() {
// cancel the requestAnimationFrame
localCancelAnimationFrame(rAFID);
callback(getCurrentTime());
}, ANIMATION_FRAME_TIMEOUT);
};
複製代碼
這個函數咱們直接就經過函數名來理解,也就是調用requestAnimationFrame,若是超時了就改成普通調用。dom
在接下來部份內容,咱們須要預先了解requestAnimationFrame和eventLoop的知識異步
var animationTick = function(rafTime) {
// scheduledHostCallback也就是callback
if (scheduledHostCallback !== null) {
// 這裏是連續遞歸調用,直到scheduledHostCallback === null
// scheduledHostCallback會在messageChannel的port1的回調中設爲null
// 由於requestAnimationFrameWithTimeout會加入event loop,因此這裏不是普通遞歸,而是每一幀執行一次
// 注意當下一幀執行了animationTick時,以前的animationTick已經計算出了nextFrameTime
requestAnimationFrameWithTimeout(animationTick);
} else {
// No pending work. Exit.
isAnimationFrameScheduled = false;
return;
}
// 保持瀏覽器能保持每秒30幀,那麼每幀就是33毫秒
// activeFrameTime在模塊頂部定義,初始值爲33
// previousFrameTime的初始值也是33
// nextFrameTime就是此方法到下一幀以前能夠執行多少時間
// 若是第一次執行,nextFrameTime確定是很大的,由於frameDeadline爲0
// rafTime是當前時間戳
// 當第一次執行,nextFrameTime的值是一個包含當前時間戳,很大的值
// 當不是第一次執行frameDeadline在後面已經賦值爲rafTime + activeFrameTime
// 也就是這個公式爲new_rafTime - (old_rafTime + old_activeFrameTime) + new_activeFrameTime
// 也就是(new_rafTime - old_rafTime) + (new_activeFrameTime - old_activeFrameTime)
// 當通常狀況(也就是不走近分支1)的狀況,new_activeFrameTime === old_activeFrameTime
// 因此nextFrameTime === (new_rafTime - old_rafTime)
// 也就是兩個requestAnimationFrameWithTimeout之間的時間差,即一幀所走過的時間
// 當走過兩幀以後,發現nextFrameTime和nextFrameTime的時間都小於activeFrameTime,則斷定當前平臺的幀數更高(每幀的時間更短)
// 則走分支1修改activeFrameTime
var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
if (
nextFrameTime < activeFrameTime &&
previousFrameTime < activeFrameTime
) {
// TODO 分支1
if (nextFrameTime < 8) {
// Defensive coding. We don't support higher frame rates than 120hz.
// If the calculated frame time gets lower than 8, it is probably a bug.
nextFrameTime = 8;
}
// 這裏試探性的設置了activeFrame,由於在某些平臺下,每秒的幀數可能更大,例如vr遊戲這種狀況
// 設置activeFrameTime爲previousFrameTime和nextFrameTime中的較大者
activeFrameTime =
nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
} else {
previousFrameTime = nextFrameTime;
}
frameDeadline = rafTime + activeFrameTime;
// isMessageEventScheduled的值也是在port1的回調中設置爲false
// isMessageEventScheduled的意義就是每一幀的animationTick是否被執行完
// animationTick -> port.postMessage(設置isMessageEventScheduled爲false) -> animationTick
// 防止port.postMessage被重複調用(應該是在requestAnimationFrameWithTimeout超時的時候會出現的狀況
// 由於postMessage也是依賴event loop,可能會有競爭關係
if (!isMessageEventScheduled) {
isMessageEventScheduled = true;
// port就是port1
// postMessage是event loop下一個tick使用,因此就是frameDeadline中,其實留了空閒時間給瀏覽器執行動畫渲染
// 舉個例子: 假設當前瀏覽器爲30幀,則每幀33ms,frameDeadline爲currentTime + 33,當調用了port.postMessage,當前tick的js線程就變爲空了
// 這時候就會留給瀏覽器部分時間作動畫渲染,因此實現了requestIdleCallback的功能
// port.postMessage是留給空出js線程的關鍵
port.postMessage(undefined);
}
};
複製代碼
中間部分nextFrameTIme
的判斷是React檢查幀數的計算,咱們先忽略,關注總體。 animationTick
一開始直接scheduledHostCallback
是否爲null
,不然就繼續經過requestAnimationFrameWithTimeout
調用animationTick
自身,這是一個逐幀執行的遞歸。意思就是這個遞歸在瀏覽器在渲染下一幀的時候,纔會調用再次調用animationTick。 也就是在animationTick的調用requestAnimationFrameWithTimeout(animationTick)
以後,後面的代碼依然有時間能夠執行。由於遞歸會在下一幀由瀏覽器調用。而在animationTick最後的代碼調用了port.postMessage
,這是一個一個瀏覽器提供的APIMessageChannel
,主要用於註冊的兩端port之間相互通信,有興趣的讀者能夠本身查查。MessageChannel
的通信每次調用都是異步的,相似於EventListener
,也就是,當調用port.postMessage
,也就是告訴瀏覽器當前EventLoop的任務執行完了,瀏覽器能夠檢查一下如今如今有沒有別的任務進來(例如動畫或者用戶操做),而後插入下一個EventLoop中。(固然在EventLoop的任務隊列中,animationTick剩餘的代碼優先級會比動畫及用戶操做更高,由於排序排在前面。可是其實後面的代碼也會有根據幀時間是否足夠,執行讓出線程的操做) 遞歸的流程以下圖 函數
AnimationTick
的流程,咱們接下來看看具體的代碼,AnimationTick的參數rafTime是當前的時間戳,activeFrameTime是React假設每一幀的時間,默認值爲33,33是瀏覽器在每秒30幀的狀況下,每一幀的時間。frameDeadline默認值爲0。 咱們先跳過中間判斷看一下frameDeadline的計算公式
frameDeadline = rafTime + activeFrameTime;
複製代碼
因此frameDeadline是當前幀結束的時間。 nextFrameTime則是下一幀預計剩餘時間,咱們看nextFrameTime的計算公式,
// 此處的rafTime是下一幀執行時的currentTime
var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
複製代碼
即currentTime - 任務的expirationTime + 每個幀的時間
,也就是 每一幀的時間 - 任務預計花費的實際
,因此nextFrameTime是預計的下一幀的剩餘時間。 當咱們執行兩幀事後,previousFrameTime和nextFrameTime都計算出值,咱們就有可能走進中間的判斷若是先後兩幀的時間都比默認設置的activeFrameTime小,也就是當前代碼執行平臺的幀數可能比30幀更高,因此設置activeFrameTime爲測試出的新值。這種狀況可能出如今VR這種對幀數要求更高的環境。 接下面咱們判斷isMessageEventScheduled的布爾值,這是爲了防止保證port.postMessage(undefined);
在每一幀只調用一次。
在AnimationTick中調用port.postMessage(undefined);
以後,咱們實際上進入了channel.port1
的回調函數,
channel.port1.onmessage = function(event) {
// 設置爲false,防止animationTick的競爭關係
isMessageEventScheduled = false;
var prevScheduledCallback = scheduledHostCallback;
var prevTimeoutTime = timeoutTime;
scheduledHostCallback = null;
timeoutTime = -1;
var currentTime = getCurrentTime();
var didTimeout = false;
// 說明超過了activeFrameTime的實際(默認值33
// 說明這一幀沒有空閒時間,而後檢查任務是否過時,過時的話就設置didTimeout,用於後面強制執行
if (frameDeadline - currentTime <= 0) {
// There's no time left in this idle period. Check if the callback has
// a timeout and whether it's been exceeded.
// 查看任務是否過時,過時則強行更新
// timeoutTime就是當時的CurrentTime + timeout
// timeout是scheduleCallbackWithExpirationTime傳進來的
// 至關於currentTimeStamp + expirationTIme
if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
// Exceeded the timeout. Invoke the callback even though there's no
// time left.
// 這種過時的狀況有可能已經掉幀了
didTimeout = true;
} else {
// 沒有超時則等待下一幀再執行
// No timeout.
// isAnimationFrameScheduled這個變量就是判斷是否在逐幀執行animationTick
// 開始設置animationTick時設置爲true,animationTick結束時設置爲false
if (!isAnimationFrameScheduled) {
// Schedule another animation callback so we retry later.
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
// Exit without invoking the callback.
// 由於上一個任務沒有執行完,設置回原來的值,等animationTick繼續處理scheduledHostCallback
scheduledHostCallback = prevScheduledCallback;
timeoutTime = prevTimeoutTime;
return;
}
}
複製代碼
此處代碼用了React中經常使用的命名方式prevXXXX
,通常是在某個流程之中,先保留以前的值,在執行完某個操做以後,再還原某個值,提供給別的代碼告訴本身正在處理的階段。例如
var prevScheduledCallback = scheduledHostCallback;
scheduledHostCallback = null;
...
...
// 還原
scheduledHostCallback = prevScheduledCallback;
複製代碼
整個回調函數其實比較簡單,只有幾個分支
function flushWork(didTimeout) {
// didTimeout是指任務是否超時
// Exit right away if we're currently paused
if (enableSchedulerDebugging && isSchedulerPaused) {
return;
}
isExecutingCallback = true;
const previousDidTimeout = currentDidTimeout;
currentDidTimeout = didTimeout;
try {
if (didTimeout) {
// Flush all the expired callbacks without yielding.
while (
firstCallbackNode !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
// TODO Wrap in feature flag
// Read the current time. Flush all the callbacks that expire at or
// earlier than that time. Then read the current time again and repeat.
// This optimizes for as few performance.now calls as possible.
var currentTime = getCurrentTime();
if (firstCallbackNode.expirationTime <= currentTime) {
// 這個循環的意思是,遍歷callbackNode鏈表,直到第一個沒有過時的callback
// 因此主要意義就是將全部過時的callback馬上執行完
do {
// 這個函數有將callbackNode剝離鏈表並執行的功能, firstCallbackNode在調用以後會修改爲爲新值
// 這裏遍歷直到第一個沒有過時的callback
flushFirstCallback();
} while (
firstCallbackNode !== null &&
firstCallbackNode.expirationTime <= currentTime &&
!(enableSchedulerDebugging && isSchedulerPaused)
);
continue;
}
break;
}
} else {
// Keep flushing callbacks until we run out of time in the frame.
if (firstCallbackNode !== null) {
do {
if (enableSchedulerDebugging && isSchedulerPaused) {
break;
}
flushFirstCallback();
// shouldYieldToHost就是比較frameDeadline和currentTime,就是當前幀還有時間的話,就一直執行
} while (firstCallbackNode !== null && !shouldYieldToHost());
}
}
} finally {
isExecutingCallback = false;
currentDidTimeout = previousDidTimeout;
if (firstCallbackNode !== null) {
// There's still work remaining. Request another callback.
// callback鏈表還沒所有執行完,繼續
// ensureHostCallbackIsScheduled也是會啓動下一幀,因此不是連續調用
// 同時,isHostCallbackScheduled決定了ensureHostCallbackIsScheduled的行爲,
// 在此分支中isHostCallbackScheduled === true, 因此ensureHostCallbackIsScheduled會執行一個cancelHostCallback函數
// cancelHostCallback設置scheduledHostCallback爲null,能夠令上一個animationTick中止
ensureHostCallbackIsScheduled();
} else {
// isHostCallbackScheduled這個變量只會在ensureHostCallbackIsScheduled中被設置爲true
// 這個變量的意義多是表明,是否全部任務都被flush了?,由於只有firstCallbackNode === null的狀況下才會設爲false
isHostCallbackScheduled = false;
}
// Before exiting, flush all the immediate work that was scheduled.
flushImmediateWork();
}
}
複製代碼
flushFirstCallback
function flushFirstCallback() {
var flushedNode = firstCallbackNode;
// Remove the node from the list before calling the callback. That way the
// list is in a consistent state even if the callback throws.
var next = firstCallbackNode.next;
// 這裏是從鏈表中刪除firstCallbackNode的處理
if (firstCallbackNode === next) {
// 這種狀況,鏈表只有一個元素,直接清空
// This is the last callback in the list.
firstCallbackNode = null;
next = null;
} else {
// 這個操做就是從鏈表中刪除掉firstCallbackNode
var lastCallbackNode = firstCallbackNode.previous;
firstCallbackNode = lastCallbackNode.next = next;
next.previous = lastCallbackNode;
}
flushedNode.next = flushedNode.previous = null;
// Now it's safe to call the callback.
// 像下面這種,先將currentXXX賦值給previousXXX,而後再講previousXXX賦值給currentXXX,多是由於同時還有別的地方須要使用到currentXXX,留意一下
// 也有多是要保證代碼執行成功以後,才修改currentXXX的值
var callback = flushedNode.callback;
var expirationTime = flushedNode.expirationTime;
var priorityLevel = flushedNode.priorityLevel;
var previousPriorityLevel = currentPriorityLevel;
var previousExpirationTime = currentExpirationTime;
currentPriorityLevel = priorityLevel;
currentExpirationTime = expirationTime;
var continuationCallback;
try {
continuationCallback = callback();
} finally {
currentPriorityLevel = previousPriorityLevel;
currentExpirationTime = previousExpirationTime;
}
// A callback may return a continuation. The continuation should be scheduled
// with the same priority and expiration as the just-finished callback.
if (typeof continuationCallback === 'function') {
var continuationNode: CallbackNode = {
callback: continuationCallback,
priorityLevel,
expirationTime,
next: null,
previous: null,
};
// Insert the new callback into the list, sorted by its expiration. This is
// almost the same as the code in `scheduleCallback`, except the callback
// is inserted into the list *before* callbacks of equal expiration instead
// of after.
// 這個鏈表插入順序的區別在於,遇到expirationTime相等的element,scheduleCallback會設置在該element後面
// 而此函數會設置在該element前面
if (firstCallbackNode === null) {
// This is the first callback in the list.
firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
} else {
var nextAfterContinuation = null;
var node = firstCallbackNode;
do {
// 和scheduleCallback函數惟一的區別就是這個等號
if (node.expirationTime >= expirationTime) {
// This callback expires at or after the continuation. We will insert
// the continuation *before* this callback.
nextAfterContinuation = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);
if (nextAfterContinuation === null) {
// No equal or lower priority callback was found, which means the new
// callback is the lowest priority callback in the list.
nextAfterContinuation = firstCallbackNode;
} else if (nextAfterContinuation === firstCallbackNode) {
// The new callback is the highest priority callback in the list.
firstCallbackNode = continuationNode;
ensureHostCallbackIsScheduled();
}
var previous = nextAfterContinuation.previous;
previous.next = nextAfterContinuation.previous = continuationNode;
continuationNode.next = nextAfterContinuation;
continuationNode.previous = previous;
}
}
}
複製代碼
下面記錄了部分核心變量的解釋,只做爲幫助閱讀使用
P.S. 在下面變量的命名中,包含hostCallback
,host能夠理解爲主要的。包含scheduled
能夠理解爲是否正在處理
* head
* next7 next1
* next6 next2
* next5 next3
* next4
*
複製代碼
假設咱們的鏈表有8個元素,他們會按照expirationTime從小到大排序,也就是head(firstCallbackNode)的expirationTime最小,next7的expirationTime最大。
TODO:補充expirationTime和優先級大小的關係以及貼上issue地址
在scheduler的代碼中,咱們會常常看到一個判斷
firstCallbackNode === null // 若是成立則鏈表爲空
複製代碼
這個其實就是在判斷鏈表是否爲空,由於任何對鏈表的刪除和增長操做,都會更新firstCallbackNode的值,保證firstCallbackNode不爲null,除非整個鏈表已經沒有任何元素了。
boolean 用於判斷當前callback是否超時。
boolean 判斷整個scheduler是否正在flush
callback,flush
能夠理解爲執行callback。 這個變量在函數flushWork中設置爲true,當callback執行完以後設置爲false
boolean 判斷是否進入了requestHostCallback,requestHostCallback會開啓animationTick,進行每個幀的任務調度。當調用到flushWork直到鏈表中的callback處理結束,設爲false。 主要用於當一個callback處理後產生continuationCallback時,而這個continuationCallback再次成爲firstCallbackNode(也就是expirationTime最小的callback),須要從新調用ensureHostCallbackIsScheduled時,將當前的相關變量重置
scheduledHostCallback = null;
isMessageEventScheduled = false;
timeoutTime = -1;
複製代碼
function 就是函數flushWork,這個變量可能會被置null,用於animationTick判斷是否停止遞歸
在animationTick中,判斷經過messageChannel傳輸的回調是否執行了
當前正在處理的callback(firstCallbackNode)的expirationTime
是否處於animationTick的逐幀遞歸中
是否正在執行flushWork(也就是是否正在處理callback)