react的Scheduler源码解析
react版本:18.3.1
位置:packages\scheduler\src\forks\Scheduler.js
是什么?
对不同优先级任务进行调度,使高优先级先执行,低优先级后执行。
解决的问题?
解决页面卡顿问题,js单线程,大任务会阻塞浏览器的绘制,导致页面卡顿。
怎么解决的?
把任务分片,在浏览器空余时间去执行,通过优先级分配顺序
怎么使用?
const { unstable_scheduleCallback } = require("scheduler")
const tasks = [1,1,2,2,3,3,4,4,1,2,3,4,1,2,3,4,3,2,1,1,1,1,1]
tasks.forEach((priority , i) => {
unstable_scheduleCallback(priority , ()=>{
console.log(`优先级${priority}` , `第${i}任务`)
})
})
console.log("同步任务")
Promise.resolve().then(res => console.log("微任务"))
// 输出:
// 同步任务
// 微任务
// 优先级1,第0个任务
// 优先级1,第1个任务
// 优先级1,第8个任务
// 优先级1,第12个任务
// 优先级1,第18个任务
// 优先级1,第19个任务
// 优先级1,第20个任务
// 优先级1,第21个任务
// 优先级2,第2个任务
// 优先级2,第3个任务
// 优先级2,第9个任务
// 优先级2,第13个任务
// ...
Scheduler中的优先级
export const ImmediatePriority = 1; // 超时优先级:需要立即执行的任务
export const UserBlockingPriority = 2; // 用户阻塞优先级:用户交互相关任务
export const NormalPriority = 3; // 普通优先级:常规更新任务
export const LowPriority = 4; // 低优先级:可延迟执行的任务
export const IdlePriority = 5; // 空闲优先级:在空闲时执行的任务
Scheduler中的全局变量
var maxSigned31BitInt = 1073741823;
// 不同优先级对应时间,会和当前时间相加,计算出过期时间点
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
var taskQueue = []; // 立即执行任务队列
var timerQueue = []; // 延迟执行任务队列
var taskIdCounter = 1; // 任务ID计数器
var isSchedulerPaused = false;
var currentTask = null; // 当前正在执行的任务对象
var currentPriorityLevel = NormalPriority; // 当前调度器优先级
var isPerformingWork = false; // 是否正在执行工作循环
var isHostCallbackScheduled = false; // 是否已安排工作循环调度
var isHostTimeoutScheduled = false; // 是否已安排延迟任务检查
小顶堆
本质就是一棵二叉树结构,这里用数组模拟的,它的堆顶永远维持着最小值(最高任务),对外暴露3个方法,push 往堆中塞入一个元素,pop 弹出堆顶元素,peek获取堆顶元素
用节点的 sortIndex作为判断依据 ,如果比较不了,就是用ID,也就是顺序了
function compare(a, b) {
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}
Scheduler暴露出的api
export {
ImmediatePriority as unstable_ImmediatePriority,
UserBlockingPriority as unstable_UserBlockingPriority,
NormalPriority as unstable_NormalPriority,
IdlePriority as unstable_IdlePriority,
LowPriority as unstable_LowPriority,
unstable_runWithPriority,
unstable_next,
unstable_scheduleCallback, // 入口方法,开启调度
unstable_cancelCallback, // 取消任务
unstable_wrapCallback, // 包裹任务
unstable_getCurrentPriorityLevel,
shouldYieldToHost as unstable_shouldYield,
unstable_requestPaint,
unstable_continueExecution,
unstable_pauseExecution,
unstable_getFirstCallbackNode,
getCurrentTime as unstable_now, // 获取当前时间戳
forceFrameRate as unstable_forceFrameRate,
};
入口函数
根据优先级计算出当前任务的过期时间。
创建当前任务对象
过期时间和当前时间戳比较,延时任务加入到timerQueue,其他任务加入taskQueue
当前任务为延时,开启定期检查timerQueue中任务的定时器
当前任务为task任务,开启任务调度
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime(); // 获取当前时间
var startTime; // 计算任务开始时间,注意delay参数,如果有delay参数,则表示当前任务为延时任务,会加入timerQueue中
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:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
var expirationTime = startTime + timeout; // 计算任务过期时间
var newTask = { // 创建任务对象
id: taskIdCounter++, // 任务id,自增
callback, // 任务本身回调函数
priorityLevel, // 任务优先级
startTime, // 任务开始时间
expirationTime, // 任务过期时间
sortIndex: -1, // 用于小顶堆排序的索引,会用来计算优先级,会被赋值expirationTime
};
if (startTime > currentTime) { // 延时任务,加入timerQueue中
newTask.sortIndex = startTime;
push(timerQueue, newTask);
cancelHostTimeout(); // 取消定期检查timerQueue
requestHostTimeout(handleTimeout, startTime - currentTime); // 开启定期检查timerQueue中的任务,检查是否到期
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask); // 加入到 taskQueue
requestHostCallback(flushWork); // 开启任务调,传入flushWork
}
return newTask;
}
通过Scheduler调度的任务,都会在下一个宏任务中去执行,schedulePerformWorkUntilDeadline的定义会根据环境情况去实现
依次检查setImmediate,MessageChannel,setTimeout是否可用
function requestHostCallback(callback) {
scheduledHostCallback = callback; // 注意,这是全局变量,callback为flushWork
// 开启任务调度循环, 此函数会根据环境选择合适的异步调度API,实现宏任务
schedulePerformWorkUntilDeadline();
}
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') { // setImmediate
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') { // MessageChannel
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else { // setTimeout
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
循环执行taskQueue中的任务
会判断任务池中的任务
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) { // 判断当前任务是否为空
const currentTime = getCurrentTime();
//全局的startTime,用来记录当前这批任务的调度开始时间,用来判断是否用完切片用的。
startTime = currentTime;
const hasTimeRemaining = true;
let hasMoreWork = true;
try {
// 执行flushWork
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
// 如果task队列中还有,继续在下一个宏任务中调度
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
} else {
isMessageLoopRunning = false;
}
needsPaint = false;
};
function flushWork(hasTimeRemaining, initialTime) {
isHostCallbackScheduled = false; // 重置调度状态,防止重复调度
// 如果有定时检查timeQueue,取消检查定时器
if (isHostTimeoutScheduled) {
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true; // 标记正在执行工作循环中
const previousPriorityLevel = currentPriorityLevel; // 保存当前优先级
try {
// ...
return workLoop(hasTimeRemaining, initialTime);
} finally {
currentTask = null; // 清理当前任务引用 - 当前任务已完成或暂停
currentPriorityLevel = previousPriorityLevel; // 恢复原始优先级
isPerformingWork = false; // 标记 工作循环已完成
}
}
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime); // 检查timerQueue中是否有延时任务
currentTask = peek(taskQueue); // 取出第一个任务,最高优先级
while (currentTask !== null) {
// 如果 expriationTime > currentTime 说明任务还没有过期,
// 过期任务,不会再调用,因为调度是宏任务
// shouldYieldToHost 判断浏览器是否还有空余时间(5ms),没有就跳出本次循环,下个宏任务执行
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null; // 先置空任务,防止重复调用
currentPriorityLevel = currentTask.priorityLevel;
// 判断当前任务是否过期,chuandaocallback中,供用户使用
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// 执行任务,可能有返回的新任务
const continuationCallback = callback(didUserCallbackTimeout);
// 任务未完成,继续赋值回调函数
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
}
// 任务完成了,删除任务
else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime); // 执行完一个任务,就去检查timerQueue中是否有延时任务
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue); // 下次循环
}
// 如果taskQueue还有任务,返回true,表示还有任务未完成, 需要继续调度
if (currentTask !== null) {
return true;
} else { // taskQueue已空,检查timerQueue中是否有延时任务
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); // 开定时器,执行handleTimeout
}
return false;
}
}
advanceTimers用来检查timerQueue中是否有到期的延时任务,如果有则转移到taskQueue中
这个函数会在workLoop开始循环前调用,在一个任务执行结束后调用。
并且也会开始定时器,定时调用
// 循环检查timerQueue中是否有到期的延时任务,如果有则转移到taskQueue中
function advanceTimers(currentTime) {
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
pop(timerQueue);
} else if (timer.startTime <= currentTime) { // 检查是否过期了
pop(timerQueue); // 从timerQueue删除当前任务
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer); // 加入到 taskQueue
}
timer = peek(timerQueue);
}
}
检查timerQueue
当没有开启调度,并且taskQueue有任务,就开启调度
当taskQueue没有任务,timerQueue有任务就再开启定时器,检查timerQueue
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime);
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork); // 开启调度
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); // 延时检查
}
}
}
}
shouldYieldToHost用来判断浏览器空余时间是否用完,是否要退出调度
var frameInterval = frameYieldMs;
function shouldYieldToHost() {
var timeElapsed = exports.unstable_now() - startTime;
if (timeElapsed < frameInterval) {
return false; // 说明不应该中断,时间片还没有用完
}
return true; // 说明时间片已经用完了
}