base: react-16.10.2
首先查看README文件,官方介绍Scheduler是在浏览器环境中对任务进行协作调度的一个库。因此实际上,这个库跟react并没有关联,它只是实现了对一连串的任务进行排序,然后在适当的时间去执行任务的这样一个功能。
1. 任务优先级
在SchedulerPriorities文件中,定义了任务的优先级。
export const NoPriority = 0; // 没有优先级,一些任务初始化的时候可能会用到
export const ImmediatePriority = 1; //最高优先级
export const UserBlockingPriority = 2; //用户阻塞型优先级
export const NormalPriority = 3; //普通优先级
export const LowPriority = 4; // 低优先级
export const IdlePriority = 5; // 空闲优先级
这5种优先级都有自己的timeout。
// 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;
timeout的意思就是说,在安排任务顺序的时候,我们是通过获取当前的页面时间加上这个timeout,计算出这个任务的过期时间。如果页面时间超过了这个任务的过期时间,则表明这个任务需要立即执行。
因此上述优先级中,ImmediatePriority的过期时间小于当前页面时间,会立即执行,而USER_BLOCKING_PRIORITY的过期时间则是在当前页面时间的250ms之后。
2. 任务排序
通过unstable_scheduleCallback方法来进行任务的排序。
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime(); // 获取当前页面时间
var startTime; // 就是currentTime,如果options里面有delay则加上delay
var timeout; // 任务优先级里所对应的timeout,如果options里面有timeout则就是该timeout
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
timeout =
typeof options.timeout === 'number'
? options.timeout
: timeoutForPriorityLevel(priorityLevel);
} else {
timeout = timeoutForPriorityLevel(priorityLevel);
startTime = currentTime;
}
var expirationTime = startTime + timeout;
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
//....todo
}
可以看到该函数接受3个参数,任务的优先级,任务的回调,以及一些options(delay, timeout)。
上部分的代码的话,就是计算出这个任务的startTime与expirationTime。然后生成一个task,保存了各种信息。
接下来就是对该任务进行排序了。
// Tasks are stored on a min heap
var taskQueue = [];
var timerQueue = [];
function unstable_scheduleCallback(priorityLevel, callback, options) {
// ...省略
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// ...省略
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// ...省略
}
return newTask;
}
可以看到代码对任务的开始时间进行了判断,如果大于现在的页面时间,也就是说这个任务现在还不能执行的,会放到timerQueue队列里面,否则放入到taskQueue里面去。
这两个队列的通过Min Heap来存储任务。也就是说队列的第一个任务,就是我们最先需要执行的任务。排序的依据的话在timerQueue里面是startTime, 在taskQueue里面是expirationTime。
每次在执行任务的时候,都会先去判断timerQueue里面的任务是否已经到了需要执行的时间了,如果到了则会取出来放入taskQueue里面,进行排序,最后取出taskQueue的任务依次去执行。
3. 任务调度
安排好任务之后,会调用requestHostCallback(flushWork)来注册flushWork回调,并在适当的时间进行调用。
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
requestHostCallback = function(callback) {
scheduledHostCallback = callback;
// enableMessageLoopImplementation 为 true
if (enableMessageLoopImplementation) {
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
} else {
if (!isRAFLoopRunning) {
// Start a rAF loop.
isRAFLoopRunning = true;
requestAnimationFrame(rAFTime => {
onAnimationFrame(rAFTime);
});
}
}
};
这里的enableMessageLoopImplementation写死为true,也就是说目前的调度器的更新策略并不是使用requestAnimationFrame,并在frame里面通过postMessage来进行的了。而是直接通过一开始就调用postMessage来直接调度。
port1接受到message之后,进入performWorkUntilDeadline来执行任务。
const performWorkUntilDeadline = () => {
if (enableMessageLoopImplementation) {
// 有任务需要执行
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// 计算出本次更新的结束时间
frameDeadline = currentTime + frameLength;
const hasTimeRemaining = true;
try {
// 开始进行任务的执行
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
// 调度结束,如果还有任务需要执行,则postMessage等待进行下一次的调度,否则进行收尾工作
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
port.postMessage(null);
}
} catch (error) {
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 {
// ...省略
}
};
代码中用到了frameLength,也就是说每一次更新持续的时间,目前使用的是5ms。也就是说是应用可以保持在一个大于120帧超高的频率上更新。
scheduledHostCallback保存的就是之前传进来的flushWork这个函数。这个函数会调用workLoop来执行任务。
// hasTimeRemaining为true,initialTime为currentTime
// 传hasTimeRemaining为false,为只运行过期任务
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime); // 之前讲过的提取timerQueue的任务到taskQueue里面去
currentTask = peek(taskQueue); // 获取第一个任务
while ( currentTask !== null ) {
// 这个做的判断是说当前任务的过期时间还是大于现在的时间,并且没有多于的时间了或者shouldYieldToHost返回为true的时候,则暂停执行。
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;
// 执行任务
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
// 如果任务返回的是一个function,则替换任务的回调函数,否则删除该任务
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// ...省略
}
上面的代码有调用到shouldYieldToHost来判断这次任务的调度是否需要停止。
let maxFrameLength = 300;
shouldYieldToHost = function() {
const currentTime = getCurrentTime();
// frameDeadline为上面计算出来的这一次更新需要停止的时间
if (currentTime >= frameDeadline) {
// There's no time left in the frame. We may want to yield control of
// the main thread, so the browser can perform high priority tasks. The
// main ones are painting and user input. If there's a pending paint or
// a pending input, then we should yield. But if there's neither, then
// we can yield less often while remaining responsive. We'll eventually
// yield regardless, since there could be a pending paint that wasn't
// accompanied by a call to `requestPaint`, or other main thread tasks
// like network events.
// 如果现在浏览器需要重绘或者浏览器有需要处理的输入事件时,返回true,表示这次更新需要停止了
if (needsPaint || scheduling.isInputPending()) {
// There is either a pending paint or a pending input.
return true;
}
// There's no pending input. Only yield if we've reached the max
// frame length.
return currentTime >= frameDeadline + maxFrameLength;
} else {
// There's still time left in the frame.
return false;
}
};
4. 总结
- 根据任务优先级来计算出任务的过期时间
- 根据任务的过期时间,开始时间来加入timerQueue或者taskQueue队列
- 创建任务调度机制,通过postMessage来在下一事件循环中执行任务
- 计算出该一次执行任务的持续时间,执行taskQueue里面的任务
- 执行结束,如果还有任务需要执行,则继续通过postMessage来安排下一次任务调度