React scheduler
什么是React scheduler呢?
這就是react可以做到在diff的時候,用來做任務(wù)分配的機(jī)制。因?yàn)閖s是單線程的,所以如果一次執(zhí)行任務(wù)太多的話,如果在這期間用戶過來點(diǎn)擊個按鈕,輸入個數(shù)字什么的,瀏覽器可能毫無反應(yīng),這樣用戶可能會以為瀏覽器卡死啦。
現(xiàn)在瀏覽器提供了一個接口requestidlecallback, mdn描述(https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)。具體就是現(xiàn)在瀏覽器可以讓我們有機(jī)會在瀏覽器空閑的時候,用來執(zhí)行一段任務(wù)。
但是react的scheduler并沒有使用該接口,而是自己實(shí)現(xiàn)了一個requestidlecallback的ployfill。
為什么沒有這么用呢?
據(jù)說是因?yàn)榧嫒輪栴},或者是react目前沒有看到瀏覽器廠商對它強(qiáng)烈的支持,或者是其他原因。
react是使用的requestAnimationFrame來模擬實(shí)現(xiàn)的requestidlecallback。
React scheduler 流程
這里通過把所有的任務(wù)通過雙向鏈表鏈接起來,類似如下圖:

然后通過requestAnimationFrame或者setTimeout來獲取瀏覽器在每幀的空閑時間來循環(huán)處理所有的任務(wù),直到鏈表為空為止。
React scheduler 代碼
代碼文件(node_modules/scheduler/cjs/scheduler.development.js)
unstable_scheduleCallback
// 組成雙向鏈表,開始安排任務(wù)
function unstable_scheduleCallback(callback, deprecated_options) {
// currentEventStartTime 初始值為-1,也就是初始使用當(dāng)前的時間
var startTime =
currentEventStartTime !== -1
? currentEventStartTime
: exports.unstable_now();
// 過期時間
var expirationTime;
// 這里很簡單,就是根據(jù)不同的優(yōu)先級,賦予不同的過期時間
if (
typeof deprecated_options === "object" &&
deprecated_options !== null &&
typeof deprecated_options.timeout === "number"
) {
// FIXME: Remove this branch once we lift expiration times out of React.
expirationTime = startTime + deprecated_options.timeout;
} else {
switch (currentPriorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
expirationTime = startTime + USER_BLOCKING_PRIORITY;
break;
case IdlePriority:
expirationTime = startTime + IDLE_PRIORITY;
break;
case LowPriority:
expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
}
}
// 組裝成新的node
var newNode = {
callback: callback,
priorityLevel: currentPriorityLevel,
expirationTime: expirationTime,
next: null,
previous: null
};
// Insert the new callback into the list, ordered first by expiration, then
// by insertion. So the new callback is inserted any other callback with
// equal expiration.
if (firstCallbackNode === null) {
// This is the first callback in the list.
// 開始安排
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled();
} else {
var next = null;
var node = firstCallbackNode;
do {
if (node.expirationTime > expirationTime) {
// The new callback expires before this one.
next = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);
if (next === null) {
// 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) {
// The new callback has the earliest expiration in the entire list.
// 插在最前面
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled();
}
var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}
return newNode;
}
這里的unstable_scheduleCallback就是requestIdleCallback的代替者,不過也可以看到源碼里邊標(biāo)注了unstable,表示不是穩(wěn)定的,以后隨時會改。
unstable_scheduleCallback的功能很簡單,就是根據(jù)傳入的callback和options,計(jì)算出過期時間,然后組成雙向任務(wù)鏈表,然后開始通過ensureHostCallbackIsScheduled()來安排任務(wù)循環(huán)執(zhí)行。
ensureHostCallbackIsScheduled
然后來看ensureHostCallbackIsScheduled這個函數(shù),這個也很簡單,首先判斷是否任務(wù)已經(jīng)開始循環(huán)安排了,如果是,則退出,如果沒有,則重置條件,重新開始去請求循環(huán)安排任務(wù)。
// 是否已經(jīng)開始安排任務(wù)
function ensureHostCallbackIsScheduled() {
// 有一個callback正在進(jìn)行
if (isExecutingCallback) {
// Don't schedule work yet; wait until the next time we yield.
return;
}
// firstCallbackNode的過期時間是最早的
// 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.
// 取消其它存在的host callback
cancelHostCallback();
}
// 開始安排任務(wù)隊(duì)列
requestHostCallback(flushWork, expirationTime);
}
可以看到這里最后走到了requestHostCallback(flushWork, expirationTime);
這里的flushwork是schedule的一個刷新任務(wù)隊(duì)列函數(shù),等會再看。先看下
requestHostCallback
requestHostCallback
這里requestHostCallback根據(jù)傳入的callback和過期時間確定下一步執(zhí)行那些操作,如果當(dāng)天正在執(zhí)行任務(wù),或者是過期時間小于0,則通過port.postMessage發(fā)送信息,來立即執(zhí)行任務(wù)更新。
這里的port.postMessage是
var channel = new MessageChannel();
var port = channel.port2;
這里可以理解為一個通道,就是當(dāng)在scheduler中如果想要立即執(zhí)行任務(wù)鏈表的更新,就可以通過port.postMessage來發(fā)送一個信息,通過channel.port1.onmessage開接受信息,并且立即開始執(zhí)行任務(wù)鏈表的更新,類似一個發(fā)布訂閱,當(dāng)想更新鏈表的時候,只需要發(fā)送個信息就可以了。
scheduler里邊就是通過MessageChannel來完成通知和執(zhí)行任務(wù)鏈表更新操作的。
requestHostCallback 里邊如果沒有到到期時間且還還沒有開始通過isAnimationFrameScheduled來訂閱瀏覽器的空閑時間,則通過requestAnimationFrameWithTimeout(animationTick)去訂閱。
// 開始安排任務(wù), callback就是剛才的flushwork函數(shù), absoluteTimeout是傳入的過期時間
requestHostCallback = function(callback, absoluteTimeout) {
// 準(zhǔn)備開始的callback,開始執(zhí)行的回調(diào)函數(shù)
scheduledHostCallback = callback;
// 過期時間
timeoutTime = absoluteTimeout;
if (isFlushingHostCallback || absoluteTimeout < 0) {
// ASAP 盡快
// Don't wait for the next frame. Continue working ASAP, in a new event.
port.postMessage(undefined);
} else if (!isAnimationFrameScheduled) {
// 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);
}
};
因?yàn)樽詈蠖紩叩綀?zhí)行任務(wù)鏈表刷新的地方,也就是最后都會走到和port.postMessage(undefined)這里發(fā)出的請求,然后通過channel.port1.onmessage這里來處理的時候,所以這里暫時先不看這里,等到最后再看這邊的代碼,目前先先看下requestAnimationFrameWithTimeout
requestAnimationFrameWithTimeout
這里主要是使用requestAnimationFrame,但是會有requestAnimationFrame不起作用的情況下,使用setTimeout。
// requestAnimationFrame does not run when the tab is in the background. If
// we're backgrounded we prefer for that work to happen so that the page
// continues to load in the background. So we also schedule a 'setTimeout' as
// a fallback.
// TODO: Need a better heuristic for backgrounded work.
var ANIMATION_FRAME_TIMEOUT = 100;
var rAFID;
var rAFTimeoutID;
var requestAnimationFrameWithTimeout = function(callback) {
// schedule rAF and also a setTimeout
rAFID = localRequestAnimationFrame(function(timestamp) {
// cancel the setTimeout
localClearTimeout(rAFTimeoutID);
callback(timestamp);
});
rAFTimeoutID = localSetTimeout(function() {
// cancel the requestAnimationFrame
localCancelAnimationFrame(rAFID);
callback(exports.unstable_now());
}, ANIMATION_FRAME_TIMEOUT);
};
代碼也很簡單,這里傳入的callback是animationTick,去看下animationTick的代碼
animationTick
這個函數(shù)也很簡單,就是保持循環(huán)訂閱瀏覽器的空閑時間,同時動態(tài)的更新每幀的時間,因?yàn)閞eact里邊剛開始的默認(rèn)的每幀的時間是33ms,這里也就是默認(rèn)30fps,但是react里邊可以根據(jù)實(shí)際的fps來動態(tài)的更新每幀的時間,通過這里,
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;
}
// If one frame goes long, then the next one can be short to catch up.
// If two frames are short in a row, then that's an indication that we
// actually have a higher frame rate than what we're currently optimizing.
// We adjust our heuristic dynamically accordingly. For example, if we're
// running on 120hz display or 90hz VR display.
// Take the max of the two in case one of them was an anomaly due to
// missed frame deadlines.
activeFrameTime =
nextFrameTime < previousFrameTime
? previousFrameTime
: nextFrameTime;
可以看到,這里react也做了一個最小的限制,最小的時候,每幀的時間是8ms
var animationTick = function(rafTime) {
if (scheduledHostCallback !== null) {
// Eagerly schedule the next animation callback at the beginning of the
// frame. If the scheduler queue is not empty at the end of the frame, it
// will continue flushing inside that callback. If the queue *is* empty,
// then it will exit immediately. Posting the callback at the start of the
// frame ensures it's fired within the earliest possible frame. If we
// waited until the end of the frame to post the callback, we risk the
// browser skipping a frame and not firing the callback until the frame
// after that.
requestAnimationFrameWithTimeout(animationTick);
} else {
// No pending work. Exit.
isAnimationFrameScheduled = false;
return;
}
// 一幀之內(nèi)還剩時間rafTime - frameDeadline
var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
if (
nextFrameTime < activeFrameTime &&
previousFrameTime < activeFrameTime
) {
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;
}
// If one frame goes long, then the next one can be short to catch up.
// If two frames are short in a row, then that's an indication that we
// actually have a higher frame rate than what we're currently optimizing.
// We adjust our heuristic dynamically accordingly. For example, if we're
// running on 120hz display or 90hz VR display.
// Take the max of the two in case one of them was an anomaly due to
// missed frame deadlines.
activeFrameTime =
nextFrameTime < previousFrameTime
? previousFrameTime
: nextFrameTime;
} else {
previousFrameTime = nextFrameTime;
}
frameDeadline = rafTime + activeFrameTime;
if (!isMessageEventScheduled) {
isMessageEventScheduled = true;
port.postMessage(undefined);
}
};
可以看到這里最后也是通過port.postMessage(undefined)來觸發(fā)任務(wù)鏈表隊(duì)列。
channel.port1.onmessage
現(xiàn)在去看下真正開始更新任務(wù)鏈表的時候,到底做了些什么?
代碼也比較簡單,最重要的就是這里調(diào)用了在開始的ensureHostCallbackIsScheduled傳入的requestHostCallback(flushWork, expirationTime)的fulshWork,也就是onmessage里邊的prevScheduledCallback(didTimeout)
// We use the postMessage trick to defer idle work until after the repaint.
// 我們使用postMessage 技巧來將空閑工作推遲到重繪之后
var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = function(event) {
isMessageEventScheduled = false;
// 重制, timeout過期時間
var prevScheduledCallback = scheduledHostCallback;
var prevTimeoutTime = timeoutTime;
scheduledHostCallback = null;
timeoutTime = -1;
var currentTime = exports.unstable_now();
var didTimeout = false;
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.
// 已經(jīng)執(zhí)行過了
if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
// Exceeded the timeout. Invoke the callback even though there's no
// time left.
didTimeout = true;
} else {
// No timeout.
// 沒有執(zhí)行過
if (!isAnimationFrameScheduled) {
// Schedule another animation callback so we retry later.
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
// Exit without invoking the callback.
scheduledHostCallback = prevScheduledCallback;
timeoutTime = prevTimeoutTime;
return;
}
}
if (prevScheduledCallback !== null) {
isFlushingHostCallback = true;
try {
prevScheduledCallback(didTimeout);
} finally {
isFlushingHostCallback = false;
}
}
};
flushWork
終于到了真正開始干活的地方啦,這里也很簡單,就是通過循環(huán),在給定的時間里去調(diào)用flushFirstCallback(),并且在最后去執(zhí)行最緊急的callback
function flushWork(didTimeout) {
isExecutingCallback = true;
// currentDidTimeout 初始值為false
var previousDidTimeout = currentDidTimeout;
currentDidTimeout = didTimeout;
try {
if (didTimeout) {
// Yield 退讓
// Flush all the expired callbacks without yielding.
while (firstCallbackNode !== null) {
// 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 = exports.unstable_now();
// 刷新列表
if (firstCallbackNode.expirationTime <= currentTime) {
do {
flushFirstCallback();
} while (
firstCallbackNode !== null &&
firstCallbackNode.expirationTime <= currentTime
);
continue;
}
break;
}
} else {
// Keep flushing callbacks until we run out of time in the frame.
if (firstCallbackNode !== null) {
do {
flushFirstCallback();
// deadline < current, 空閑時間到期
} while (firstCallbackNode !== null && !shouldYieldToHost());
}
}
} finally {
isExecutingCallback = false;
currentDidTimeout = previousDidTimeout;
if (firstCallbackNode !== null) {
// There's still work remaining. Request another callback.
ensureHostCallbackIsScheduled();
} else {
isHostCallbackScheduled = false;
}
// Before exiting, flush all the immediate work that was scheduled.
flushImmediateWork();
}
}
flushFirstCallback
這里就是一些鏈表的操作,刪除或者插入,或者重新去請求安排時間等等,
// 更新第一個任務(wù)
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;
if (firstCallbackNode === next) {
// This is the last callback in the list.
// 最后一個啦,全部設(shè)置為空
firstCallbackNode = null;
next = null;
} else {
// 從鏈表中刪掉firstCallbackNode
var lastCallbackNode = firstCallbackNode.previous;
firstCallbackNode = lastCallbackNode.next = next;
next.previous = lastCallbackNode;
}
// 全部設(shè)置為空,獨(dú)立出來
flushedNode.next = flushedNode.previous = null;
// Now it's safe to call the callback.
// 獲取各種屬性
var callback = flushedNode.callback;
var expirationTime = flushedNode.expirationTime;
var priorityLevel = flushedNode.priorityLevel;
// 當(dāng)前的等級和過期時間, 簡單的交換
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.
// 有可能返回一個函數(shù)
if (typeof continuationCallback === "function") {
var continuationNode = {
callback: continuationCallback,
priorityLevel: priorityLevel,
expirationTime: 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.
// 很簡單,插入進(jìn)去,根據(jù)過期時間
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 {
// 比較過期時間,
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;
}
}
}
flushImmediateWork
最后更新所有最緊急的任務(wù),
function flushImmediateWork() {
if (
// Confirm we've exited the outer most event handler
currentEventStartTime === -1 &&
firstCallbackNode !== null &&
firstCallbackNode.priorityLevel === ImmediatePriority
) {
isExecutingCallback = true;
try {
do {
flushFirstCallback();
} while (
// Keep flushing until there are no more immediate callbacks
firstCallbackNode !== null &&
firstCallbackNode.priorityLevel === ImmediatePriority
);
} finally {
isExecutingCallback = false;
if (firstCallbackNode !== null) {
// There's still work remaining. Request another callback.
ensureHostCallbackIsScheduled();
} else {
isHostCallbackScheduled = false;
}
}
}
}
總結(jié)
這里的代碼好像每個react版本的都會變,不過基本的原理基本上都是差不多,變得只不過是些細(xì)節(jié)。