這是從測試看react源碼的第一篇,先從一個獨立的 Scheduler 模塊入手。正如官方所說,Scheduler模塊是一個用於協做調度任務的包,防止瀏覽器主線程長時間忙於運行一些事情,關鍵任務的執行被推遲。用於 react 內部,如今將它獨立出來,未來會成爲公開API
node
"test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js"
複製代碼
- testRegex: '/__tests__/[^/]*(\\.js|\\.coffee|[^d]\\.ts)$',
+ testRegex: '/__tests__/Scheduler-test.js',
moduleFileExtensions: ['js', 'json', 'node', 'coffee', 'ts'],
rootDir: process.cwd(),
- roots: ['<rootDir>/packages', '<rootDir>/scripts'],
+ roots: ['<rootDir>/packages/scheduler'],
複製代碼
在開始以前,先了解一下jest的運行環境,省的一會找不到代碼對應位置react
jest.mock('scheduler', () => require('scheduler/unstable_mock'));
, 那麼以後 require('scheduler')
其實就是引入的 scheduler/unstable_mock.js
throw new Error('This module must be shimmed by a specific build.');
,那麼就去 scripts/jest/setupHostConfig.js
中去查看到底使用了哪一個文件。scheduler 用於調度任務,防止瀏覽器主線程長時間忙於運行一些事情,關鍵任務的執行卻被推遲。那麼任務就要有一個優先級,每次優先執行優先級最高的任務。在 schuduler 中有兩個任務隊列,taskQueue 和 timerQueue 隊列,是最小堆實現的優先隊列。數組第一項永遠爲優先級最高的子項。算法
it('flushes work incrementally', () => {
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('B'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('C'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('D'));
expect(Scheduler).toFlushAndYieldThrough(['A', 'B']);
expect(Scheduler).toFlushAndYieldThrough(['C']);
expect(Scheduler).toFlushAndYield(['D']);
});
複製代碼
Scheduler.unstable_yieldValue
函數比較簡單,就是將參數push到一個數組中,用於結果的比較。 scheduleCallback
中會構造 Task, 以下json
// 構造Task
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
複製代碼
並將其加入 taskQueue 或者 timerQueue。若是當前沒有任務在執行則調用 requestHostCallback(flushWork);
用於執行任務。 在測試環境中 requestHostCallback 的實現以下:數組
export function requestHostCallback(callback: boolean => void) {
scheduledCallback = callback;
}
複製代碼
僅僅是保存了 flushWork 函數, 並無執行。 傳入的 flushWork
函數返回 boolean 變量,當 true 時爲有 task 能夠執行,可是當前時間切片已結束,將空出主線程給瀏覽器。其中執行 workLoop
,workLoop
是任務執行的真正地方。其首先會從當前 timerQueue 中取出定時已經到期的timer,將其加入到 taskQueue 中。接來下就是取出 taskQueue 中的任務並執行。workLoop
代碼以下:瀏覽器
// 取出已經到時的 timer,放入 taskQueue 中。
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
const callback = currentTask.callback;
if (callback !== null) {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
markTaskRun(currentTask, currentTime);
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// 當前任務執行過程當中被中斷,則將以後須要繼續執行的callback保存下來
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
// 已經取消的任務,調用unstable_cancelCallback方法取消任務
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
複製代碼
循環取出 taskQueue 中的任務,直到 taskQueue 爲空,或者當前時間切片時間已到而且任務也尚未過時,中斷循環。把任務放到下一個時間切片執行。注意一下 shouldYieldToHost()
,這個也是判斷是否繼續執行的條件,以後會用到。bash
1-4 行代碼結果就是將四個 task 推入taskQueue,等待執行。而後去看一下 matcher toFlushAndYieldThrough
, 位置在 scripts/jest/matchers/schedulerTestMatchers.js
。函數
function toFlushAndYieldThrough(Scheduler, expectedYields) {
assertYieldsWereCleared(Scheduler);
Scheduler.unstable_flushNumberOfYields(expectedYields.length);
const actualYields = Scheduler.unstable_clearYields();
return captureAssertion(() => {
expect(actualYields).toEqual(expectedYields);
});
}
複製代碼
執行與 expectedYields(就是test裏的['A', 'B'],['C'],['D'] ) 數量相同的任務,看結果是否相等 unstable_flushNumberOfYields
代碼以下,這裏的cb就是 flushWork,第一個參數爲 true表示當前切片有剩餘時間oop
export function unstable_flushNumberOfYields(count: number): void {
if (isFlushing) {
throw new Error('Already flushing work.');
}
if (scheduledCallback !== null) {
const cb = scheduledCallback;
expectedNumberOfYields = count;
isFlushing = true;
try {
let hasMoreWork = true;
do {
// 執行任務
hasMoreWork = cb(true, currentTime);
} while (hasMoreWork && !didStop);
if (!hasMoreWork) {
scheduledCallback = null;
}
} finally {
expectedNumberOfYields = -1;
didStop = false;
isFlushing = false;
}
}
}
複製代碼
那麼什麼時候中止呢?記得以前的 shouldYieldToHost
函數嗎,在 schedulerHostConfig.mock.js 在實現了此函數,而且添加了一個 didStop 變量用於測試中控制數量,當達到 expectedNumberOfYields 數量時退出循環。代碼以下:測試
export function shouldYieldToHost(): boolean {
if (
(expectedNumberOfYields !== -1 &&
yieldedValues !== null &&
yieldedValues.length >= expectedNumberOfYields) ||
(shouldYieldForPaint && needsPaint)
) {
// We yielded at least as many values as expected. Stop flushing.
didStop = true;
return true;
}
return false;
}
複製代碼
it('cancels work', () => {
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
const callbackHandleB = scheduleCallback(NormalPriority, () =>
Scheduler.unstable_yieldValue('B'),
);
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('C'));
// 取消任務即把task.callback設置爲null,
cancelCallback(callbackHandleB);
expect(Scheduler).toFlushAndYield([
'A',
// B should have been cancelled
'C',
]);
});
複製代碼
將task callback 設置爲 null 就是取消任務,這部分在 workLoop裏有判斷
it('executes the highest priority callbacks first', () => {
// 這加入taskQueue時,用最小堆排序算法排序的
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('A'));
scheduleCallback(NormalPriority, () => Scheduler.unstable_yieldValue('B'));
// Yield before B is flushed
expect(Scheduler).toFlushAndYieldThrough(['A']);
scheduleCallback(UserBlockingPriority, () =>
Scheduler.unstable_yieldValue('C'),
);
scheduleCallback(UserBlockingPriority, () =>
Scheduler.unstable_yieldValue('D'),
);
// C and D should come first, because they are higher priority
expect(Scheduler).toFlushAndYield(['C', 'D', 'B']);
});
複製代碼
在 scheduler 中定義了6中優先級:
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
複製代碼
每種優先級又對應了不一樣的超時時間:
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
複製代碼
其中 NoPriority 和 NormalPriority 的超時時間同樣,這能夠在 scheduler.js中看到:
function timeoutForPriorityLevel(priorityLevel) {
switch (priorityLevel) {
case ImmediatePriority:
return IMMEDIATE_PRIORITY_TIMEOUT;
case UserBlockingPriority:
return USER_BLOCKING_PRIORITY_TIMEOUT;
case IdlePriority:
return IDLE_PRIORITY_TIMEOUT;
case LowPriority:
return LOW_PRIORITY_TIMEOUT;
case NormalPriority:
default:
return NORMAL_PRIORITY_TIMEOUT;
}
}
複製代碼
在新建任務時會根據當前時間和超時時間計算出過時時間,taskQueue 是按照過時時間排序的。優先級高的任務,過時時間就會越小,因此會先被執行。
it('expires work', () => {
scheduleCallback(NormalPriority, didTimeout => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue(`A (did timeout: ${didTimeout})`);
});
scheduleCallback(UserBlockingPriority, didTimeout => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue(`B (did timeout: ${didTimeout})`);
});
scheduleCallback(UserBlockingPriority, didTimeout => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue(`C (did timeout: ${didTimeout})`);
});
// Advance time, but not by enough to expire any work
Scheduler.unstable_advanceTime(249);
expect(Scheduler).toHaveYielded([]);
// Schedule a few more callbacks
scheduleCallback(NormalPriority, didTimeout => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue(`D (did timeout: ${didTimeout})`);
});
scheduleCallback(NormalPriority, didTimeout => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue(`E (did timeout: ${didTimeout})`);
});
// Advance by just a bit more to expire the user blocking callbacks
// currentTime被設置成249 + 1 而 UserBlockingPriority 的timeout爲250,因此超時執行
Scheduler.unstable_advanceTime(1);
expect(Scheduler).toFlushExpired([
'B (did timeout: true)',
'C (did timeout: true)',
]);
// Expire A
// 250 + 100 + 100 + 4600 = 5050 > 5000(NORMAL_PRIORITY_TIMEOUT)
Scheduler.unstable_advanceTime(4600);
expect(Scheduler).toFlushExpired(['A (did timeout: true)']);
// Flush the rest without expiring
expect(Scheduler).toFlushAndYield([
'D (did timeout: false)',
'E (did timeout: true)',
]);
});
複製代碼
UserBlockingPriority 類型的任務,新建 task 時設置過時時間爲當前時間 + USER_BLOCKING_PRIORITY_TIMEOUT(250), 而 NormalPriority 則爲 當前時間 + NORMAL_PRIORITY_TIMEOUT(5000)。 unstable_advanceTime 做用是就是增量當前時間。好比 unstable_advanceTime(100),意味這當前時間增長了 100ms,若是當前時間大於過時時間,則任務過時。 在最後調用 scheduledCallback 時, 代碼以下:
export function unstable_flushExpired() {
if (isFlushing) {
throw new Error('Already flushing work.');
}
if (scheduledCallback !== null) {
isFlushing = true;
try {
const hasMoreWork = scheduledCallback(false, currentTime);
if (!hasMoreWork) {
scheduledCallback = null;
}
} finally {
isFlushing = false;
}
}
}
複製代碼
在其中調用 scheduledCallback(也就是 flushWork) 時將 hasTimeRemaining 參數設置爲false,當前時間切片未有剩餘,僅任務過時纔會執行,未過時則放到下一個時間切片執行。這裏的第一個測試 Scheduler.unstable_advanceTime(249),有些多餘,無論設置成多少都不會有變化。 這裏須要注意的是D, E 任務過時時間爲 5249,因此最後 D 任務沒有過時 而 E 任務過時了。
it('continues working on same task after yielding', () => {
// workLoop 中若是callback執行以後返回函數,會把返回的函數再次存入task對象的callback中保存下來
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('A');
});
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('B');
});
let didYield = false;
const tasks = [
['C1', 100],
['C2', 100],
['C3', 100],
];
const C = () => {
while (tasks.length > 0) {
const [label, ms] = tasks.shift();
Scheduler.unstable_advanceTime(ms);
Scheduler.unstable_yieldValue(label);
if (shouldYield()) {
didYield = true;
return C;
}
}
};
scheduleCallback(NormalPriority, C);
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('D');
});
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('E');
});
// Flush, then yield while in the middle of C.
expect(didYield).toBe(false);
expect(Scheduler).toFlushAndYieldThrough(['A', 'B', 'C1']);
expect(didYield).toBe(true);
// When we resume, we should continue working on C.
expect(Scheduler).toFlushAndYield(['C2', 'C3', 'D', 'E']);
});
複製代碼
這裏主要看下任務C,其 callback 其最後又返回了自身,至關於任務的子任務,被中斷以後,下一個時間切片繼續執行子任務。子任務繼承了父任務的全部參數,當執行完當前子任務以後,僅僅須要設置父任務 callback 函數爲下一個子任務 callback,在 workLoop 中的相關代碼以下:
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// 當前任務執行過程當中被中斷,則將以後須要繼續執行的callback保存下來
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
複製代碼
目前講解了5個測試,而關於 scheduler 中的測試還有不少,更多的細節還須要本身去分析,但願本文能夠起到拋磚引玉的做用,給你起了個閱讀源碼的頭,勿急勿躁。下次會分享 schedulerBrowser-test,模擬瀏覽器環境對 scheduler 進行測試。