基于react v17.3 scheduler0.20.1 功能:
- 时间切片
- 优先级调度
requestIdleCallback 和 requestAnimationFrame 无法实现react团队的需求,所以自己实现了 Scheduler。
- 缺陷: 调度任务有延迟 ,执行时间不确定,兼容性不好 -- issues-11330, issues-11171 , issues-21662
调度任务 -- 创建一个宏任务
基于事件队列,创建宏任务不会卡死主线程,主线程将归还浏览器,执行相关渲染。
MessageChanel
相等于0s延迟的setTimeout,并且支持Web Worker
const channel = new MessageChannel();
const port = channel.port2;
// 通过onmessage来创建宏任务,执行performWorkUntilDeadline
// performWorkUntilDeadline功能:执行taskQueue中的所有task
channel.port1.onmessage = performWorkUntilDeadline;
// 函数功能:创建一个宏任务 执行performWorkUntilDeadline
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
setTimeout
不支持MessageChannel时备用。
在浏览器中,实际最小延迟为4s
schedulePerformWorkUntilDeadline = () => {
// setTimeout 执行执行performWorkUntilDeadline
localSetTimeout(performWorkUntilDeadline, 0);
};
每个宏任务给多少秒执行时间?
默认给 5s 执行时间 。 也可以会通过fps动态调整(forceFrameRatereact目前没启用)。
通过主线程运行了多长时间 减去 任务 开始时间, 提供判断是否需要让出主线程函数。
const frameYieldMs = 5; // 每帧5秒执行时间,也就是阻塞主线程5秒
let frameInterval = frameYieldMs;
// 是否要让出 主线程。 即判断当前分配的执行时间是否用完
function shouldYieldToHost() {
// getCurrentTime: 是获取主线程已运行时间,(performance或者Date实现)
// startTime: 每次performWorkUntilDeadline时 赋值getCurrentTime()
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
return false;
}
// InputPending 的优化 忽略
// ...
return true;
}
优先级调度
此优先级 和 react中的lane不同。
react 中优先级调度任务,都会使用 unstable_scheduleCallback
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 获取当前时间 (主线程已执行时间)
var currentTime = getCurrentTime();
// 任务的 开始时间
var startTime;
// react中没有使用options。 即startTime = currentTime
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
// -1
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
// 250
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
// maxSigned31BitInt 1073741823 V8中32位系统的最大整数大小。
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
// 10000
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
// 5000
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
// 任务过期时间
// 之后会判断,任务过期时间 小于 当前时间 就会被执行。
var expirationTime = startTime + timeout;
// 任务节点
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
// react中没有使用过options,自然也没用timerQueue
if (startTime > currentTime) {
// 延时任务 放入 timerQueue
newTask.sortIndex = startTime;
// 小顶堆排序,最早过期的排最前面
push(timerQueue, newTask);
// 若taskQueue中无任务,且newTask是timerQueu中将会最早过期的
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
if (isHostTimeoutScheduled) {
// 若已有定时任务,则清除
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// 调度一个定时器宏任务 handleTimeout
// handleTimeout :
// 1.会将timerQueue中的startTime小于currentTime的任务,加入taskQueue
// 2.若当前无任务执行,taskQueue不为空则requestHostCallback(flushWork);
// 若taskQueeu为null,调度下一个timerQueue,若也为空则什么也不干
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// 非延时任务,放入taskQueue
newTask.sortIndex = expirationTime;
// 小顶堆排序,最早过期的排最前面
push(taskQueue, newTask);
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
// 若无清空taksQueu的调度函数执行,
// 则通过开始调度 flushWork
//
// flushWork : 会将分情况将所有 taks 执行。
// 1. 已过期taks 执行
// 2. 未过期taks,再次调度flushWork重复到1,2. 直到taksQueue清空
requestHostCallback(flushWork);
}
}
return newTask;
}
清空taskQueue -- performWorkUntilDeadline
// 简化后的函数,只留了核心代码
const performWorkUntilDeadline = () => {
// scheduledHostCallback有taks时,会被赋值flushWork
// 值不为空,说明有taks需要调度
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
startTime = currentTime;
const hasTimeRemaining = true;
let hasMoreWork = true;
try {
// scheduledHostCallback有taks时,会被赋值flushWork
// 就是执行flushWork
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
if (hasMoreWork) {
// 若还有task(未过期任务) 则继续递调度
//会一直调度到 taskQueue为空
schedulePerformWorkUntilDeadline();
} else {
scheduledHostCallback = null;
}
}
}
};
// flushWork
// 简化后的函数,只留了核心代码
function flushWork(hasTimeRemaining, initialTime) {
const previousPriorityLevel = currentPriorityLevel;
try {
// while执行 taskQueue
return workLoop(hasTimeRemaining, initialTime);
} finally {
currentPriorityLevel = previousPriorityLevel;
}
}
//workLoop
//简化后的函数。
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime); // 检测timerQueue,将其中过期的timer 移到taskQueue
currentTask = peek(taskQueue); // 拿出第一个过期taskQueue
while ( currentTask !== null ) {
if (
// taks 按优先级设定的执行时间未到
currentTask.expirationTime > currentTime && shouldYieldToHost())
) {
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// 去除task中的callback,执行
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// 若返回值为函数,则taks更新callback,继续调度 未从taskQueue中推出
currentTask.callback = continuationCallback;
} else {
if (currentTask === peek(taskQueue)) {
// task完成 推出taskQueue
pop(taskQueue);
}
}
// 检测timerQueue,将其中过期的timer 移到taskQueue
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
// 下一个task
currentTask = peek(taskQueue);
}
if (currentTask !== null) {
// 若还有task,则返回true,performWorkUntilDeadline中将会继续调度
// taks未在上面执行,说明此taks的expirationTime未到
return true;
} else {
// 没有task,则调度timer
// react中暂时未启用timer,全部为task
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
总结
通过shouldYieldToHost 判断是否有执行时间,有则通过 unstable_scheduleCallback 进行优先级调度。
将需要调度的函数,创建task push到小顶堆(按时间排序)中。
只要有task,就进行轮询(创宏任务轮询)taskQueue 若过期,则立即执行。
未过期,则再次调度上述轮询,直到 taskQueue 清空。
通过宏任务轮询清除taskQueue 减少主线程阻塞。