优先
本章是手写React Scheduler异步任务调度源码系列的第四篇,上一篇可以查看 【React Scheduler源码第三部分】React Scheduler原理及手写源码
.本章介绍在调度器中实现任务优先级和高优先级任务队列插入相关的源码
优先
以我们平时的需求调度为例,高优先级的需求会先启动,而在开发过程中,总会有高优先级的需求插队。那么如何衡量需求的优先级呢?一般来说,高优先级的需求需要尽快完成,尽快上线。因此,高优先级的需求总是比低优先级的需求提早提出,即高优先级的deadline会更早。例如,在今天(9 月 8 日)安排需求 A、B、C 和 D 时:
- D的优先级比较高,2天后提交考试,考试日期为9月10日。
- 后面是B,5天后提交考试,考试日期为9月13日
- 然后是C,10天后测试,测试日期为9月18日
- 最后是A,20天后提交考试,考试日期为9月28日
在甘特图中,这些需求会标出每个需求的开始日期、截止日期等信息,然后项目管理人员会根据需求的优先级(提出提案的日期)进行排序,优先级高的将首先执行。在这个过程中,如果有新的高优先级需求,比如E需要在9月15日提出,项目经理需要对其进行重新排序,然后发现需求E需要在C之前和B之后执行。
同样,在React调度中,当我们通过scheduleCallback添加任务时,需要记录任务的开始时间、截止时间等信息,然后根据任务的截止时间进行排序。期限越小,优先级越高。需要尽快实施。
如何计算截止日期?我们可以在调度的时候传入这个任务的deadline吗,比如
scheduleCallback(新日期(“2022-09-08 18:45:34”),任务);
scheduleCallback(new Date("2022-09-08 19:20:00"), task);
复制代码
真的不会有人这样设计 API 吗?
其实和需求调度类似,我们只需要指明需求的过期时间。比如2天后提出测试,过期日期不就是当前时间+2天吗?同样,当我们调度一个任务时,我们只需要告诉调度器任务到期的时间,比如200毫秒、1000毫秒或者50000毫秒,而不需要开发者手动计算截止时间:
scheduleCallback(1000ms, 任务);
scheduleCallback(200ms, 任务);
scheduleCallback(500ms, 任务);
scheduleCallback(600ms, 任务);
scheduleCallback(500ms, 任务);
复制代码
由于传递具体值不够语义化,我们可以定义几个优先级枚举,这些枚举值代表不同的过期时间,比如:
// 下面的过期时间单位是毫秒
常量 maxSigned31BitInt = 1073741823; // 最大整数
常量 IMMEDIATE_PRIORITY_TIMEOUT = - 1; // 过期时间 - 1毫秒,超高优先级,需要立即执行
常量 USER_BLOCKING_PRIORITY_TIMEOUT = 250;
常量 NORMAL_PRIORITY_TIMEOUT = 5000;
常量 LOW_PRIORITY_TIMEOUT = 10000;
常量 IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; // 永不过期,最低优先级
// 优先
常量立即优先级 = 1;
常量用户阻塞优先级 = 2;
常量 NormalPriority = 3;
常量低优先级 = 4;
常量空闲优先级 = 5;
复制代码
然后我们可以在调用 scheduleCallback 的时候传入对应的优先级
scheduleCallback(NormalPriority, 任务);
复制代码
现在,让我们开始修改上一节中的代码以支持优先级调度
调度回调
根据我们前面提到的,当调用 scheduleCallback 调度任务任务时, scheduleCallback 必须执行以下步骤
- 获取当前任务调度的时间startTime
- 根据优先级转换为超时
- 根据 startTime 和 timeout 计算任务的过期时间
- 将任务加入队列taskQueue,还需要按照任务的优先级排序,即按照expirationTime
- 触发消息通道事件并在异步事件中处理任务
在这些步骤中,第四步需要根据expirationTime进行排序,这就需要我们每次通过scheduleCallback添加任务时重新排序。所以我们还需要一个排序算法。这里我简单实现如下:
// 每次插入任务时,都需要重新排序以确定新的优先级。就像不插入需求一样,需要根据deadline重新调度才能确定新的优先级
// 高优先级任务优先
// 在react调度器源码中,使用了最小堆排序算法。这里为了简化,我们就不那么费解了。
功能推送(队列,任务){
队列。推(任务);
队列。排序((a,b)=> {
返回一个。排序索引 - b。排序索引;
});
}
复制代码
在调度器源代码中,使用了最小堆排序算法。这里我简单的通过数组的sort方法来实现下排序算法。
函数 scheduleCallback(优先级,回调){
// 1.获取任务开始调度的时间startTime
常量开始时间 = 新日期()。获取时间();
让超时;
// 2.根据优先级转换为对应的超时时间
开关(优先级){
案例立即优先级:
超时 = IMMEDIATE_PRIORITY_TIMEOUT;
休息;
案例用户阻塞优先级:
超时 = USER_BLOCKING_PRIORITY_TIMEOUT;
休息;
情况空闲优先级:
超时 = IDLE_PRIORITY_TIMEOUT;
休息;
案例低优先级:
超时 = LOW_PRIORITY_TIMEOUT;
休息;
案例 NormalPriority:
默认:
超时 = NORMAL_PRIORITY_TIMEOUT;
休息;
}
// 3.根据startTime和timeout计算任务的deadline
常量过期时间 = 开始时间 + 超时;
让新任务 = {
回调:回调,
优先级,
开始时间,
过期时间:过期时间,
sortIndex:过期时间,
};
// 4.通过push方法将任务加入任务队列,并根据expirationTime重新排序
推(任务队列,新任务);
// 5. 触发消息通道事件
如果(!isHostCallbackScheduled){
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
返回新任务;
}
复制代码
为什么需要 push 方法?每次添加任务时push方法都会重新排序,这也解决了我们高优先级任务排队的问题,比如下面的demo,一开始我们通过scheduleCallback添加了两个相同优先级的任务,在异步的时候宏 在任务事件中执行 printA 时,会添加另一个高优先级的 printE。此时 printE 在 printB 之前执行
函数 printA() {
scheduleCallback(ImmediatePriority, printE);
}
scheduleCallback(NormalPriority, printA);
scheduleCallback(NormalPriority, printB);
复制代码
工作循环
由于我们已经介绍了任务过期时间和优先级相关的东西,所以我们在执行每个任务的时候,都需要告诉用户该任务是否已经过期。如果开始执行任务的时间大于任务的过期时间,则表示该任务已经过期。
如果任务已经过期,即使当前宏任务事件执行时间已经超过5毫秒,任务也应该在当前事件中执行,而不是在下一个事件循环中处理。所以在workLoop中你需要做以下事情:
-
判断当前任务是否过期
-
如果过期,则必须在当前宏任务事件中执行
-
如果还没有过期,则需要判断当前宏任务事件的执行时间是否超过5毫秒。如果超过则退出循环,剩下的任务将在下一个宏任务事件中处理。
-
计算当前任务是否逾期
函数工作循环(初始时间){
让当前时间 = 初始时间;当前任务=任务队列[0];
而(当前任务){
if (currentTask.expirationTime > currentTime && shouldYield()) {
// 当前currentTask还没有过期,但是当前宏任务事件已经到了执行的deadline,也就是我们需要
// 将控制权返回给浏览器,剩下的任务在下一个事件循环中继续
// console.log("yield");
休息;
}
常量回调 = 当前任务。打回来;
// 问题1 为什么需要判断回调
if (typeof callback === "function") {
// 问题2 为什么需要将回调重置为null
当前任务。回调=空;
常量 didUserCallbackTimeout = 当前任务。过期时间 <= 当前时间;
回调(didUserCallbackTimeout);
当前时间 = 新日期()。获取时间();
// 问题3 为什么要判断currentTask是否等于taskQueue[0]
if (currentTask === taskQueue[0]) {
任务队列。转移();
}
} 别的 {
任务队列。转移();
}
当前任务=任务队列[0];
}
如果(当前任务){
// 如果 taskQueue 中还有工作,则返回 true
返回真;
} 别的 {
isHostCallbackScheduled = false;
返回假;
}
}
复制代码
注意上面的问题1到3,其实这是为了解决高优先级任务的插队问题。例如,在下面的测试用例 2 中,如果我们嵌套调用 scheduleCallback 以插入更高优先级的任务:
函数 printA(didTimeout) {
scheduleCallback(UserBlockingPriority, printC);
安慰。 log("一个didTimeout:",didTimeout);
}
函数 printB(didTimeout) {
安慰。 log("B didTimeout:", didTimeout);
}
函数 printC(didTimeout) {
安慰。 log("C didTimeout:", didTimeout);
}
scheduleCallback(NormalPriority, printA);
scheduleCallback(NormalPriority, printB);
复制代码
一开始是通过scheduleCallback添加两个优先级相同的任务,此时taskQueue = [taskA, taskB],然后开始在宏任务事件中调用workLoop来执行任务。 TaskA先执行,taskA执行时,通过 scheduleCallback(UserBlockingPriority, printC);
插入了更高优先级的任务taskC,此时taskQueue=[taskC, taskA, taskB],所以不能简单的通过 任务队列.shift()
删除第一项,所以有如下判断:
// 问题3 为什么要判断currentTask是否等于taskQueue[0]
if (currentTask === taskQueue[0]) {
任务队列。转移();
}
复制代码
那么我们应该如何删除已经执行的taskA呢?这是问题2,我们在回调执行开始时将回调重置为null:currentTask.callback = null。当while循环再次遍历到taskA时,由于taskA.callback为null,直接调用taskQueue.shift()删除即可。因为1-3题都是解决高优先级任务切队的问题
测试用例
用例 1:具有不同优先级的任务
通过 scheduleCallback 调度不同优先级的任务,优先级高的先执行
函数 printA(didTimeout) {
常量开始 = 新日期()。获取时间();
while ( new Date().getTime() - start < 7) {}
安慰。 log("一个didTimeout:",didTimeout);
}
函数 printB(didTimeout) {
常量开始 = 新日期()。获取时间();
while ( new Date().getTime() - start < 3) {}
安慰。 log("B didTimeout:", didTimeout);
}
函数 printC(didTimeout) {
常量开始 = 新日期()。获取时间();
while ( new Date().getTime() - start < 4) {}
安慰。 log("C didTimeout:", didTimeout);
}
函数 printD(didTimeout) {
常量开始 = 新日期()。获取时间();
while ( new Date().getTime() - start < 7) {}
安慰。 log("D didTimeout:", didTimeout);
}
函数 printE(didTimeout) {
常量开始 = 新日期()。获取时间();
while ( new Date().getTime() - start < 10) {}
安慰。 log("E didTimeout:", didTimeout);
}
scheduleCallback(IdlePriority, printA);
scheduleCallback(LowPriority, printB);
scheduleCallback(NormalPriority, printC);
scheduleCallback(UserBlockingPriority, printD);
scheduleCallback(ImmediatePriority, printE);
复制代码
打印:
E didTimeout:真
D didTimeout: false
C didTimeout: false
B didTimeout: false
一个didTimeout: false
复制代码
用例 2:高优先级任务队列问题
先通过 scheduleCallback 添加两个普通优先级任务,此时 taskQueue = [taskA, taskB],然后在执行 printA 时嵌套调用
scheduleCallback 插入优先级更高的任务taskC,此时taskQueue=[taskC, taskA, taskB]
函数 printA(didTimeout) {
常量开始 = 新日期()。获取时间();
while ( new Date().getTime() - start < 7) {}
scheduleCallback(UserBlockingPriority, printC);
安慰。 log("一个didTimeout:",didTimeout);
}
函数 printB(didTimeout) {
常量开始 = 新日期()。获取时间();
while ( new Date().getTime() - start < 3) {}
安慰。 log("B didTimeout:", didTimeout);
}
函数 printC(didTimeout) {
常量开始 = 新日期()。获取时间();
while ( new Date().getTime() - start < 4) {}
安慰。 log("C didTimeout:", didTimeout);
}
scheduleCallback(NormalPriority, printA);
scheduleCallback(NormalPriority, printB);
复制代码
控制台输出:
一个didTimeout: false
C didTimeout: false
B didTimeout: false
复制代码
用例 3:如果任务过期则强制执行
这次我们添加了三个执行时间为 1000 毫秒的任务,它们都有 UserBlockingPriority 优先级,所以它们的过期时间超时都是 250 毫秒。同时为了方便我们查看多次触发的宏任务事件,我们在 performWorkUntilDeadline 中添加了一条日志
功能 performWorkUntilDeadline() {
安慰。 log("触发的 performWorkUntilDeadline 执行");
// ...
}
复制代码 函数 printA(didTimeout) {
常量开始 = 新日期()。获取时间();
while ( new Date().getTime() - start < 1000) {}
安慰。 log("一个didTimeout:",didTimeout);
}
函数 printB(didTimeout) {
常量开始 = 新日期()。获取时间();
while ( new Date().getTime() - start < 1000) {}
安慰。 log("B didTimeout:", didTimeout);
}
函数 printC(didTimeout) {
常量开始 = 新日期()。获取时间();
while ( new Date().getTime() - start < 1000) {}
安慰。 log("C didTimeout:", didTimeout);
}
scheduleCallback(UserBlockingPriority, printA);
scheduleCallback(UserBlockingPriority, printB);
scheduleCallback(UserBlockingPriority, printC);
复制代码
控制台输出:
触发了performWorkUntilDeadline执行
一个didTimeout: false
B didTimeout:真
C didTimeout:真
复制代码
因此可以看出,即使这三个任务的执行时间是1秒,也远超5毫秒,但是因为都超时了,所以都是在当前宏任务事件中执行的。
概括
至此,我们已经实现了按优先级调度任务和为高优先级任务切队列的问题。完整的源代码可以查看 这里 .下一篇继续介绍实现延迟任务的问题。
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议。转载请附上原文出处链接和本声明。
这篇文章的链接: https://homecpp.art/5810/7533/1637
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具