从测试看react源码_scheduler

这是从测试看react源码的第一篇,先从一个独立的 Scheduler 模块入手。正如官方所说,Scheduler模块是一个用于协作调度任务的包,防止浏览器主线程长时间忙于运行一些事情,关键任务的执行被推迟。用于 react 内部,现在将它独立出来,将来会成为公开API

环境配置

  • 下载源码到本地,我使用的分支是 master(4c7036e80)
  • 由于 react 测试代码太多,如果想要仅测试 scheduler 部分代码,需要修改一下jest配置
  • 打开 package.json,可以看到 test 命令的配置文件是 config.source.js

    1
    "test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js"
  • 在其中我们可以看到又引入了 config.base.js 文件,接下来就是修改 config.base.js

    1
    2
    3
    4
    5
    6
    - 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'],
  • 在项目根目录下运行 yarn run test,不出意外会看到测试成功运行

jest 相关介绍

在开始之前,先了解一下jest的运行环境,省的一会找不到代码对应位置

  • jest.mock()用于mock某个模块,比如jest.mock('scheduler', () => require('scheduler/unstable_mock'));, 那么之后 require('scheduler') 其实就是引入的 scheduler/unstable_mock.js
  • 如果你看到以 hostConfig 结尾的文件名中内容是 throw new Error('This module must be shimmed by a specific build.');,那么就去 scripts/jest/setupHostConfig.js中去查看到底使用了哪个文件。
  • expect(xx).Function() 这里的 Function 被称为 matcher。 在 jest 中有些默认的 matcher,比如说 toEqual,也可以自定义。scripts/jest/matchers 就是一些 react 自定义的。

miniHeap

scheduler 用于调度任务,防止浏览器主线程长时间忙于运行一些事情,关键任务的执行却被推迟。那么任务就要有一个优先级,每次优先执行优先级最高的任务。在 schuduler 中有两个任务队列,taskQueue 和 timerQueue 队列,是最小堆实现的优先队列。数组第一项永远为优先级最高的子项。

Scheduler-test.js

1. flushes work incrementally
1
2
3
4
5
6
7
8
9
10
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, 如下

1
2
3
4
5
6
7
8
9
// 构造Task
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};

并将其加入 taskQueue 或者 timerQueue。如果当前没有任务在执行则调用 requestHostCallback(flushWork); 用于执行任务。
在测试环境中 requestHostCallback 的实现如下:

1
2
3
export function requestHostCallback(callback: boolean => void) {
scheduledCallback = callback;
}

仅仅是保存了 flushWork 函数, 并没有执行。
传入的 flushWork 函数返回 boolean 变量,当 true 时为有 task 可以执行,但是当前时间切片已结束,将空出主线程给浏览器。其中执行 workLoopworkLoop 是任务执行的真正地方。其首先会从当前 timerQueue 中取出定时已经到期的timer,将其加入到 taskQueue 中。接来下就是取出 taskQueue 中的任务并执行。workLoop 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 取出已经到时的 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(),这个也是判断是否继续执行的条件,之后会用到。

1-4 行代码结果就是将四个 task 推入taskQueue,等待执行。然后去看一下 matcher toFlushAndYieldThrough, 位置在 scripts/jest/matchers/schedulerTestMatchers.js

1
2
3
4
5
6
7
8
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表示当前切片有剩余时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 数量时退出循环。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

2. cancels work
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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里有判断

3. executes the highest priority callbacks first
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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中优先级:

1
2
3
4
5
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;

每种优先级又对应了不同的超时时间:

1
2
3
4
5
6
7
8
// 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中看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 是按照过期时间排序的。优先级高的任务,过期时间就会越小,所以会先被执行。

4. expires work
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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 时, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 任务过期了。

5. continues working on same task after yielding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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 中的相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 进行测试。