这是从测试看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 进行测试。