schedulerHostConfig
# 目录
# 非 DOM 环境
标签: 了解
在非 DOM 环境中,不存在 rAF Api,因此采用了原生的 setTimeout 来模拟。这里主要用于 node 环境。
if (
// If Scheduler runs in a non-DOM environment, it falls back to a naive
// implementation using setTimeout.
// 在非 DOM 环境中,回退到 setTimeout 的原生实现,因为 非 DOM 环境没有 rAF Api。
typeof window === 'undefined' ||
// Check if MessageChannel is supported, too.
// DOM 环境要用到 MessageChannel API
// 如果 window 和MessageChannel 都不存在,就是用原生 setTimeout 模拟
typeof MessageChannel !== 'function'
) {
// If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
// fallback to a naive implementation.
let _callback = null;
let _timeoutID = null;
// 如果有 callback 就执行,执行失败就尝试空闲时段重新执行
const _flushCallback = function() {
if (_callback !== null) {
try {
const currentTime = getCurrentTime();
const hasRemainingTime = true;
_callback(hasRemainingTime, currentTime);
_callback = null;
} catch (e) {
setTimeout(_flushCallback, 0);
throw e;
}
}
};
// Scheduler 初始化的时间
const initialTime = Date.now();
// 距离初始化的时间差
getCurrentTime = function() {
return Date.now() - initialTime;
};
// 请求主线程回调
requestHostCallback = function(cb) {
if (_callback !== null) {
// Protect against re-entrancy.
// 这里将 requestHostCallback 作为定时器的回调传入,延迟 0 毫秒表示回调将在空闲时间立即执行,
// 执行时将 cb 作为参数传入
// 如果 callback 还未 flush,就尝试在空闲时间重新请求回调(依次循环)
setTimeout(requestHostCallback, 0, cb);
} else {
// callback 为空时挂载 cb,并且在空闲时间执行 callback
_callback = cb;
setTimeout(_flushCallback, 0);
}
};
// 取消主线程当前的回调,当前即时回调 由 _callback 管理
cancelHostCallback = function() {
_callback = null;
};
// 请求主线程延时回调,直接调用 setTimeout
requestHostTimeout = function(cb, ms) {
_timeoutID = setTimeout(cb, ms);
};
// 取消主线程当前延时回调,当前延时回调由 _timeoutID 管理
cancelHostTimeout = function() {
clearTimeout(_timeoutID);
};
shouldYieldToHost = function() {
return false;
};
// 请求重绘
requestPaint = forceFrameRate = function() {};
}
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
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
在 DOM 环境中,将会以 rAF 和 setTimeout 模拟。
# requestHostCallback:请求主线程回调
标签: 重要
// 请求主线程即时回调
requestHostCallback = function (callback) {
// 保存下当前的 callback
scheduledHostCallback = callback;
if (enableMessageLoopImplementation) {
// 如果当前还没有打开消息循环,说明没有其他消息回调在处理,可以打开消息循环(加锁),并发出消息
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
} else {
// 如果enableMessageLoopImplementation为 false,即没有打开消息循环的特性(不使用 messageChannel Api),就直接用 RAF 循环实现
if (!isRAFLoopRunning) {
// Start a rAF loop.
isRAFLoopRunning = true;
// rAFTime 指的是 performance.now() 的时间
requestAnimationFrame(rAFTime => {
onAnimationFrame(rAFTime);
});
}
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这个函数的作用是请求主线程即时回调。这里根据 enableMessageLoopImplementation 消息循环机制的实现是否开启分成了两种方式。我们主要来看消息循环实现的这种。
# postMessage:发送执行消息
// see: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
const channel = new MessageChannel();
const port = channel.port2;
// 当 port 上发消息时,会执行 performWorkUntilDeadline
channel.port1.onmessage = performWorkUntilDeadline;
1
2
3
4
5
2
3
4
5
这里使用了 MessageChannel API,兼容性很好。能够在 port1 和 post2 之间发送和接受消息。这里 port1 监听到 port 发消息就会执行 performWorkUntilDeadline。
# performWorkUntilDeadline: 执行回调函数
performWorkUntilDeadline 函数是调度任务的执行者,
// 执行回调任务
const performWorkUntilDeadline = () => {
if (enableMessageLoopImplementation) {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// Yield after `frameLength` ms, regardless of where we are in the vsync
// cycle(硬件设备的频率). This means there's always time remaining at the beginning of
// the message event.
// 在 frameLength 之后 yield,因此在消息时间开始时有剩余的时间
frameDeadline = currentTime + frameLength;
const hasTimeRemaining = true;
// 调用回调函数,回调函数返回布尔值表示是否中断
try {
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
// 没有更多任务,清空 scheduledHostCallback
// 结合到 react 逻辑,如果没有中断,本次回调结束
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// If there's more work, schedule the next message event at the end
// of the preceding one.
// 如果有更多的任务,继续发起下一个事件回调
// 结合 react 逻辑,如果有中断,在发一次消息,排除一个执行者
port.postMessage(null);
}
} catch (error) {
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
// 如果当前回调调用失败,继续发起下一个事件消息
port.postMessage(null);
throw error;
}
} else {
// 没有回调就把消息锁打开
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
} else {
// 如果没有打开 MessageLoop 特性,就 不会自动发起下一个事件回调,由上层函数每帧检查进行回调
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
const hasTimeRemaining = frameDeadline - currentTime > 0;
try {
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if (!hasMoreWork) {
scheduledHostCallback = null;
}
} catch (error) {
// If a scheduler task throws, exit the current browser task so the
// error can be observed, and post a new task as soon as possible
// so we can continue where we left off.
port.postMessage(null);
throw error;
}
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
}
};
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
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
# onAnimationFrame
const onAnimationFrame = rAFTime => {
// 没有回调任务
if (scheduledHostCallback === null) {
// No scheduled work. Exit.
prevRAFTime = -1;
prevRAFInterval = -1;
isRAFLoopRunning = false;
return;
}
// 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.
// 在帧首提前调度下一个动画回调。如果在帧尾调度队列非空,将会在此次回调时继续执行其他的回调。
// 如果调度队列为空,则立即退出。在帧首调节这个回调以保证他会在尽可能早的帧里被触发。
// 如果等到帧尾在提交回调,可能会导致浏览器有跳帧和没有在帧尾触发回调的风险。
isRAFLoopRunning = true;
requestAnimationFrame(nextRAFTime => {\=
// 在下一帧中清除定时器并再次执行onAnimationFrame
clearTimeout(rAFTimeoutID);
onAnimationFrame(nextRAFTime);
});
// requestAnimationFrame is throttled when the tab is backgrounded. We
// don't want to stop working entirely. So we'll fallback to a timeout loop.
// TODO: Need a better heuristic for backgrounded work.
// 当 tab 页在后台时,rAF 将会被节流。我们并不想完全停止,所以这时回退到 setTimeout。
const onTimeout = () => {
frameDeadline = getCurrentTime() + frameLength / 2;
performWorkUntilDeadline();
rAFTimeoutID = setTimeout(onTimeout, frameLength * 3);
};
rAFTimeoutID = setTimeout(onTimeout, frameLength * 3);
if (
prevRAFTime !== -1 &&
// Make sure this rAF time is different from the previous one. This check
// could fail if two rAFs fire in the same frame.
// 保证新的 rAF 并非原来的 rAF。这里意思是 rAFTime 和 prevRAFTime 不在同一帧。
rAFTime - prevRAFTime > 0.1
) {
const rAFInterval = rAFTime - prevRAFTime;
if (!fpsLocked && prevRAFInterval !== -1) {
// We've observed two consecutive frame intervals. We'll use this to
// dynamically adjust the frame rate.
// 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. 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.
// 观察到两个连续的帧间隔,将以此动态调整帧率
// 如果两次的帧时间间隔都小于 frameLength,说明浏览器 CPU 资源富足(帧率较高),将降低 yield 的间隔 frameLength。
// 根据连续两帧之间的间隔时间动态调整 frameLength
if (rAFInterval < frameLength && prevRAFInterval < frameLength) {
// 调整为两者中较大值
frameLength =
rAFInterval < prevRAFInterval ? prevRAFInterval : rAFInterval;
// 最小只能降低到 8.33
if (frameLength < 8.33) {
// Defensive coding. We don't support higher frame rates than 120hz.
// If the calculated frame length gets lower than 8, it is probably
// a bug.
// 最多只能支持 120 赫兹的帧率,
frameLength = 8.33;
}
}
}
prevRAFInterval = rAFInterval;
}
// 保存上一次的rAFTime并更新frameDeadline
prevRAFTime = rAFTime;
frameDeadline = rAFTime + frameLength;
// We use the postMessage trick to defer idle work until after the repaint.
// 发出回调消息
// 发消息只是检查有无回调任务,有则执行。
port.postMessage(null);
};
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
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
# cancelHostCallback
cancelHostCallback = function() {
scheduledHostCallback = null;
};
1
2
3
2
3
# requestHostTimeout:请求主线程延时回调
// 请求主线程延时回调。这里直接用了 setTimeout。
requestHostTimeout = function(callback, ms) {
taskTimeoutID = setTimeout(() => {
callback(getCurrentTime());
}, ms);
};
1
2
3
4
5
6
2
3
4
5
6
# cancelHostTimeout:取消主线程延迟回调
cancelHostTimeout = function() {
clearTimeout(taskTimeoutID);
taskTimeoutID = -1;
};
1
2
3
4
2
3
4
编辑 (opens new window)
上次更新: 2022/04/15, 00:23:56