React Scheduler 源码解析

之前一篇博客讲过React的更新过程,不过在那个博客中,任务调度使用的是浏览器的requestIdleCallback,而实际上React使用的自己实现的一个任务调度器,我们这次就开分析一下它的源码,以及React为什么要自己实现任务调度器。

本文基于React仓库中的16.18.6分支进行解读。

Scheduler的源码并不是很多,大概也就700多行,从作用上可以分为三部分:

  • 根据实际运行的环境差异定义 requestHostCallbackcancelHostCallbackshouldYieldToHost三个函数来实现任务的执行和取消。
  • 利用上面定义的三个函数实现根据优先级调度任务
  • 对外暴露一些接口可以添加,删除,插入一些任务

任务执行方法

关于这段代码的作用,在源码的注释里面讲过的比较清楚了,如果不想具体了解是做了什么,可以看一下它的这注释:

1
2
3
4
5
6
7
8
9
10
11
// The remaining code is essentially a polyfill for requestIdleCallback. It
// works by scheduling a requestAnimationFrame, storing the time for the start
// of the frame, then scheduling a postMessage which gets scheduled after paint.
// Within the postMessage handler do as much work as possible until time + frame
// rate. By separating the idle call into a separate event tick we ensure that
// layout, paint and other browser work is counted against the available time.
// The frame rate is dynamically adjusted.

// We capture a local reference to any global, in case it gets polyfilled after
// this module is initially evaluated. We want to be using a
// consistent implementation.

翻译一下就是:剩下的代码本质上是requestIdleCallback的填充。它的工作原理是调度requestAnimationFrame,存储帧开始的时间,然后调度绘制后调度的postMessage。在postMessage处理程序中做尽可能多的工作,直到time +帧速率。通过将空闲调用分离为一个单独的事件标记,我们确保布局、绘制和其他浏览器工作被计入可用时间。帧速率是动态调整的。我们捕获对任何全局变量的局部引用,以防它在这个模块初始计算后被填充。我们希望使用一致的实现。

接下来我们正式看一下代码,首先是几个根据当前环境判断的结果来赋值setTimeout等方法:

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
49
50
51
52
53
var localDate = Date;

// This initialization code may run even on server environments if a component
// just imports ReactDOM (e.g. for findDOMNode). Some environments might not
// have setTimeout or clearTimeout. However, we always expect them to be defined
// on the client. https://github.com/facebook/react/pull/13088
var localSetTimeout = typeof setTimeout === 'function' ? setTimeout : undefined;
var localClearTimeout =
typeof clearTimeout === 'function' ? clearTimeout : undefined;

// We don't expect either of these to necessarily be defined, but we will error
// later if they are missing on the client.
var localRequestAnimationFrame =
typeof requestAnimationFrame === 'function'
? requestAnimationFrame
: undefined;
var localCancelAnimationFrame =
typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;

var getCurrentTime;

// 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(getCurrentTime());
}, ANIMATION_FRAME_TIMEOUT);
};

if (hasNativePerformanceNow) {
var Performance = performance;
getCurrentTime = function() {
return Performance.now();
};
} else {
getCurrentTime = function() {
return localDate.now();
};
}

然后根据运行环境不同,定义 requestHostCallbackcancelHostCallbackshouldYieldToHost三个函数

Mock环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var requestHostCallback;
var cancelHostCallback;
var shouldYieldToHost;

var globalValue = null;
if (typeof window !== 'undefined') {
globalValue = window;
} else if (typeof global !== 'undefined') {
globalValue = global;
}

if (globalValue && globalValue._schedMock) {
// Dynamic injection, only for testing purposes.
var globalImpl = globalValue._schedMock;
requestHostCallback = globalImpl[0];
cancelHostCallback = globalImpl[1];
shouldYieldToHost = globalImpl[2];
getCurrentTime = globalImpl[3];
}

也就是说我们给全局对象挂载了_schedMock对象,就会进入mock的判断,然后使用我们传入的方法来定义任务执行需要的几个函数。

非浏览环境或者不支持MessageChannel时

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
else if (
// If Scheduler runs in a non-DOM environment, it falls back to a naive
// implementation using setTimeout.
typeof window === 'undefined' ||
// Check if MessageChannel is supported, too.
typeof MessageChannel !== 'function'
) {
// If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
// fallback to a naive implementation.
var _callback = null;
var _flushCallback = function(didTimeout) {
if (_callback !== null) {
try {
_callback(didTimeout);
} finally {
_callback = null;
}
}
};
requestHostCallback = function(cb, ms) {
if (_callback !== null) {
// Protect against re-entrancy.
setTimeout(requestHostCallback, 0, cb);
} else {
_callback = cb;
setTimeout(_flushCallback, 0, false);
}
};
cancelHostCallback = function() {
_callback = null;
};
shouldYieldToHost = function() {
return false;
};
}

这段代码做了如下几件事:

  • 声明了一个新的变量_callback,用于存储当前正在执行的callback
  • _flushCallback函数,当_callback不为空时,执行_callback,然后讲_callback置为空,表明当前没有正在执行的任务,可以执行其他的任务了
  • requestHostCallback函数,先是判断_callback是否为空,如果不为空,说明当前有正在执行的任务,那么就利用setTimeout来添加一个宏任务,继续执行requestHostCallback本身,这里虽然看起来像是递归调用,但是因为使用的是setTimeout,其实调用栈并不会增长。如果_callback为空,说明当前没有正在执行的任务,那么先赋值_callback,再添加宏任务去执行_flushCallback
  • cancelHostCallback函数则是将_callback置为空,取消当前的任务。其实如果当前的任务已经加入了宏任务队列,通过这种方式是没法取消的,但注意,我们宏任务队列中加入的是_flushCallback函数,如果_callback为空,其实什么都不会做。
  • shouldYieldToHost在这种情况下一定返回false。

总的来说,requestHostCallback是每次都会判断当前有没有_callback在执行,如果有就等等(这个等也是通过宏任务回调自己的方式来实现的),如果没有就添加一个宏任务,最终都会添加一个宏任务去执行_flushCallback,也就是说,理论上当有一个_callback在执行的时候,其他的requestHostCallback都在通过宏任务排队,最终执行的顺序也是requestHostCallback的顺序。

但是如果在requestHostCallback成功添加宏任务之后,执行添加的宏任务之前,清空微任务队列的时候,调用了cancelHostCallback,那其实在执行_flushCallback的时候就什么都不会做,具体可以看一下下图:

上图中的每一个方框都代表一个宏任务,方块中有两个信息,一个是当前宏任务执行的函数,一个是当前_callback的值是多少。

第一行表明的是正常情况,我们请求了两个任务,然后依次执行。第二行的是特殊情况,在执行_flushCallback之前cancel掉会发生什么

浏览器环境且支持MessageChannel

首先是打印报错信息,如果当前环境不支持requestAnimationFrame会打印报错信息,但也只是打印报错信息而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (typeof console !== 'undefined') {
// TODO: Remove fb.me link
if (typeof localRequestAnimationFrame !== 'function') {
console.error(
"This browser doesn't support requestAnimationFrame. " +
'Make sure that you load a ' +
'polyfill in older browsers. https://fb.me/react-polyfills',
);
}
if (typeof localCancelAnimationFrame !== 'function') {
console.error(
"This browser doesn't support cancelAnimationFrame. " +
'Make sure that you load a ' +
'polyfill in older browsers. https://fb.me/react-polyfills',
);
}
}

然后是几个变量,理解这几个变量的作用对于理解接下来的代码比较重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var scheduledHostCallback = null; // 当前正在执行的任务,可以类比为上面的_callback
var isMessageEventScheduled = false; // 是否有正在处理的MessageChannel消息
var timeoutTime = -1; // 当然任务的超时时间

var isAnimationFrameScheduled = false; // 是否有任务被requestAnimationFrame加入宏任务

var isFlushingHostCallback = false; // 是否有任务正在执行

var frameDeadline = 0; // 记录当前帧的到期时间,他等于rafTime + activeFrameTime,也就是requestAnimationFrame回调传入的时间,加上一帧的时间

// We start out assuming that we run at 30fps but then the heuristic tracking
// will adjust this value to a faster fps if we get more frequent animation
// frames.
var previousFrameTime = 33;
var activeFrameTime = 33;

理解了这几个变量之后,我们看一下几个工具函数:

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
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;
}

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);
}
};

这个方法主要就是做了两件事:

  • 只要scheduledHostCallback不为空,就说明当前有任务在执行,就不断通过requestAnimationFrameWithTimeout继续调用自身来重新计算每一帧的用时,也就是更新previousFrameTimeactiveFrameTime,如果当前的scheduledHostCallback为空,那就直接return,也就是停止更新每一帧的用时。
  • 同时每次执行过程中,如果isMessageEventScheduled为false,那就触发一次MessageChannel的消息。

然后是MessageChannel的处理消息的方法:

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
channel.port1.onmessage = function(event) {
isMessageEventScheduled = false;

var prevScheduledCallback = scheduledHostCallback;
var prevTimeoutTime = timeoutTime;
scheduledHostCallback = null;
timeoutTime = -1;

var currentTime = getCurrentTime();

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.
if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
// Exceeded the timeout. Invoke the callback even though there's no
// time left.
didTimeout = true;
} else {
// No timeout.
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;
}
}
};

这个处理函数主要做的事情是:

  • 判断当前时间是否已经超过了这一帧我们的规定的结束时间
    • 如果超过了判断当前任务是否有timeout并且已经开始执行了
      • 如果有并且开始执行了,那就继续执行完
      • 如果没有,那就下一帧再执行下一个任务

现在我们可以看一下这个条件下的任务执行函数了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
requestHostCallback = function(callback, absoluteTimeout) {
scheduledHostCallback = callback;
timeoutTime = absoluteTimeout;
if (isFlushingHostCallback || absoluteTimeout < 0) {
// Don't wait for the next frame. Continue working ASAP, in a new event.
port.postMessage(undefined);
} 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);
}
};

cancelHostCallback = function() {
scheduledHostCallback = null;
isMessageEventScheduled = false;
timeoutTime = -1;
};

利用上面定义的三个函数实现根据优先级调度任务

首先是声明部分变量

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
var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
var maxSigned31BitInt = 1073741823;

// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;

// Callbacks are stored as a circular, doubly linked list.
var firstCallbackNode = null;

var currentDidTimeout = false;
// Pausing the scheduler is useful for debugging.
var isSchedulerPaused = false;

var currentPriorityLevel = NormalPriority;
var currentEventStartTime = -1;
var currentExpirationTime = -1;

// This is set when a callback is being executed, to prevent re-entrancy.
var isExecutingCallback = false;

var isHostCallbackScheduled = false;

var hasNativePerformanceNow =
typeof performance === 'object' && typeof performance.now === 'function';

flushFirstCallback

首先明白一点,所有的callbackNode组成的是一个双向的环,也就是说每个node都有previous和next,并且最后一个node的next是第一个节点,第一个节点的previous是最后一个节点。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
function flushFirstCallback() {
// 保存firstCallbackNode为flushedNode等会使用
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.
firstCallbackNode = null;
next = null;
} else {
// 通过让firstCallbackNode的previous指向firstCallbackNode的next来将firstCallbackNode从任务环中删除
// 同时让firstCallbackNode指向下一个节点
var lastCallbackNode = firstCallbackNode.previous;
firstCallbackNode = lastCallbackNode.next = next;
next.previous = lastCallbackNode;
}

// flushedNode为当前要执行的任务节点
flushedNode.next = flushedNode.previous = null;

// Now it's safe to call the callback.
// 执行当前节点的callback
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.
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;
}
}
}

这段代码主要做了如下几件事:

  • 从任务节点的双向环中找出firstCallbackNode指向的节点,把它从环中取出来并执行
  • 如果执行的结果还是一个函数,也就是说callback返回了一个函数,那么就用返回的函数创建一个callbackNode,然后把它插入到环中,具体的位置就是代码中nextAfterContinuation节点之前,具体nextAfterContinuation如何得到的可以看上面的源码
    • 这里需要注意的一点是,如果nextAfterContinuationfirstCallbackNode,也就是说当前callback返回的函数所创建的新的任务节点需要插入到firstCallbackNode之前的时候,需要执行ensureHostCallbackIsScheduled

ensureHostCallbackIsScheduled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function ensureHostCallbackIsScheduled() {
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);
}

这个函数就是如果当前正在执行某个callback,也就是isExecutingCallback为true,就什么都不做。否则就取消当前的hostcallback,然后把flushWork加入任务队列中

flushWork

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
49
50
51
52
53
54
55
56
57
58
function flushWork(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) {
do {
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();
} 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();
}
}

这段代码直接看注释就好,然后flushImmediateWork的作用其实也是注释里说的,运行所有优先级为immediate的任务

flushImmediateWork

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
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;
}
}
}
}

对外暴露的方法

这里我们主要讲两个方法,一个是unstable_scheduleCallback,一个是unstable_cancelCallback

unstable_scheduleCallback

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
function unstable_scheduleCallback(callback, deprecated_options) {
var startTime =
currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();

var expirationTime;
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;
}
}

var newNode = {
callback,
priorityLevel: currentPriorityLevel,
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;
}

这段代码看起来很长,其实就是做了两件事:

  • 创建一个新的callbackNode
  • 将新的callbackNode根据时间和优先级加入到callbackNode的双向环中

unstable_cancelCallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function unstable_cancelCallback(callbackNode) {
var next = callbackNode.next;
if (next === null) {
// Already cancelled.
return;
}

if (next === callbackNode) {
// This is the only scheduled callback. Clear the list.
firstCallbackNode = null;
} else {
// Remove the callback from its position in the list.
if (callbackNode === firstCallbackNode) {
firstCallbackNode = next;
}
var previous = callbackNode.previous;
previous.next = next;
next.previous = previous;
}

callbackNode.next = callbackNode.previous = null;
}

这个也比较简单,就是从callbackNode的双向环中找出要取消的任务,然后从环中把它去掉。

最新版本的Scheduler的主要改动是:已经不是使用双向链表的方式存储所有任务了,而是使用了两个最小堆,分别叫timerQueue和taskQueue,任务会先放到timerQueue中,每次执行完taskQueue中的任务后,后检查timerQueue中有没有已经到期的任务,如果有,放入taskQueue中,然后重新开始遍历执行taskQueue中的任务,遍历过程中如果需要yield就暂时退出,然后开启设置宏任务下次继续执行