
在前端开发中,合理地管理任务调度对于保持应用流畅响应至关重要。当处理复杂的 JavaScript 任务调度时,scheduler.yield() 与 scheduler.postTask() 是两个关键 API。本文将深入探讨为何在特定场景下,开发者更倾向于使用 scheduler.yield()。
一、scheduler.yield() 的核心作用与必要性
Scheduler.yield() 方法用于在任务执行期间将控制权交还给主线程,并在稍后继续执行,且后续执行会被调度为优先级更高的任务。这使得长时间运行的任务可以被拆分,从而保持浏览器的响应速度。
当 yield() 方法返回的 Promise 被 resolve 后,任务即可继续执行。Promise resolve 后的优先级默认为 user-visible,但如果 yield() 调用发生在 Scheduler.postTask() 回调函数中则可以继承不同的优先级,这一点在后文会详述。
同时,如果 yield() 调用之后的工作发生在 postTask() 回调中,且该任务被中止,则后续工作可以被取消。
下面示例用于演示:当在主线程上需要执行耗时较长的工作,而这些工作可以分成一系列任务的情况下,可以反复调用 scheduler.yield() 来保持页面始终具有响应性。
function doWork(value) {
console.log(`work chunk ${value}`);
}
const workList = [0, 1, 2, 3, 4];
for (const work of workList) {
doWork(work);
await scheduler.yield();
}
二、scheduler.yield() 与 scheduler.postTask() 的优先级对比
scheduler.yield() 返回的 Promise 相对于其他任务的解析顺序取决于一个隐式的任务优先级。
默认情况下,scheduler.yield() 以 user-visible 优先级运行。但是,当在调用 scheduler.yield() 之后执行的后续任务与相同优先级的 scheduler.postTask() 行为略有不同。
即与相同优先级的 scheduler.postTask() 相比,scheduler.yield() 会将任务放入一个优先级更高的任务队列中。因此一个优先级为 user-visible 的 scheduler.yield() 后续任务的优先级会排在优先级更高的 user-blocking 级别的 scheduler.postTask() 任务之后,但在相同优先级的 scheduler.postTask() 任务之前。
总之,scheduler.yield() 将任务排在优先级队列的头部,scheduler.postTask() 则排在队列的末尾。在任务较少且优先级相同的情况下,scheduler.yield() 执行的任务会优先执行,从而为任务调度提供了更大的灵活性。
// 输出顺序为 user-blocking postTask 、user-visible yield 、user-visible postTask
scheduler.postTask(() => console.log("user-visible postTask"));
scheduler.postTask(() => console.log("user-blocking postTask"), {
priority: "user-blocking",
});
await scheduler.yield();
console.log("user-visible yield");
在多次调用 scheduler.yield() 时,scheduler.yield() 的后续任务会被放入优先级更高的队列特性就显得尤为重要,即第二个 scheduler.yield() 任务不会在队列中已有的任务之前执行。
即如果一个函数在第二个函数之前让出主线程,则先让出的函数会优先继续执行,例如:
async function first() {
console.log("starting first function");
await scheduler.yield();
console.log("ending first function");
}
async function second() {
console.log("starting second function");
await scheduler.yield();
console.log("ending second function");
}
first();
second();
此时控制台输出结果为:
starting first function
starting second function
ending first function
ending second function
三、在 postTask() 中调用 yield() 会继承当前任务优先级
需要注意的是,在 scheduler.postTask() 中调用 scheduler.yield() 将继承前者的任务优先级。例如,在低优先级 background 任务中调用 scheduler.yield() 之后执行的工作默认会被调度为 background 优先级。
但同样,其会被插入到优先级更高的 background 队列中,因此会在任何 background postTask() 任务之前运行。
async function backgroundWork() {
scheduler.postTask(() => console.log("background postTask"), {
priority: "background",
});
scheduler.postTask(() => console.log("user-visible postTask"), {
priority: `user-visible`,
});
// scheduler.yield() 从外部的任务继承 `background` 优先级
await scheduler.yield();
console.log("default-background yield");
}
await scheduler.postTask(backgroundWork, { priority: "background"});
此时控制台将会输入如下内容:
user-visible postTask
default-background yield
background postTask
如果开发者需要动态更改任务优先级,开发者不要显式设置 options.priority 参数,而应创建一个 TaskController,并将其 TaskSignal 传递给 options.signal。此时任务优先级将根据信号优先级初始化,之后可以使用与该信号关联的 TaskController 进行修改。
// 将 TaskController 的初始信号优先级设置为'user-blocking'
const controller = new TaskController({priority: "user-blocking"});
// 监听 controller 的信号的 prioritychange 事件
controller.signal.addEventListener("prioritychange", (event) => {
const previousPriority = event.previousPriority;
const newPriority = event.target.priority;
console.log(`Priority changed from ${previousPriority} to ${newPriority}.`);
});
// 使用 controller.signal 创建一个任务
scheduler.postTask(() => console.log("Task 1"), { signal: controller.signal });
// 使用 controller 将任务优先级设置为'background'
controller.setPriority("background");
此时控制台输出如下:
Priority changed from user-blocking to background.
Task 1
需要注意的是,上面示例中优先级是在任务执行之前更改的,但也可以在任务运行时动态更改。
四、通过 TaskController 终止外层 postTask() 来间接取消 yield()
与设置优先级类似,scheduler.yield() 调用不能直接中止,但其会继承来自其外层 scheduler.postTask() 任务的中止信号,中止该任务也会中止其中所有待处理的 scheduler.yield() 调用。
下面示例使用 TaskController 来中止包含 scheduler.yield() 的任务。
const taskController = new TaskController();
function firstHalfOfWork() {
console.log("first half of work");
taskController.abort("cancel work");
}
function secondHalfOfWork() {
// 下面代码永远不会执行
console.log("second half of work");
}
scheduler.postTask(
async () => {
firstHalfOfWork();
await scheduler.yield();
secondHalfOfWork();
},
{signal: taskController.signal}
);
在上面的例子中,abort() 发生在 scheduler.postTask() 任务启动之后,但 scheduler.yield() 调用继承了中止信号,因此 await scheduler.yield() 调用将抛出中止错误,原因为 cancel work。
五、scheduler.postTask() 的延迟执行能力
开发者可以使用 postTask() 函数的 options.delay 参数中指定一个以毫秒为单位的整数值来延迟任务。此时会将任务添加到优先级队列中,并在指定 delay 后执行,与 setTimeout() 类似。delay 时间是任务添加到调度器之前所需的最短时间,实际执行可能更长。
以下代码展示了如何添加两个任务,并设置了延迟时间。
scheduler
.postTask(() => "Task delayed by 2000ms", { delay: 2000 })
.then((taskResult) => console.log(`${taskResult}`));
scheduler
.postTask(() => "Next task should complete in about 2000ms", { delay: 1 })
.then((taskResult) => console.log(`${taskResult}`));
六、在 requestIdleCallback() 中使用 yield() 的注意事项
当从回调函数内部调用 scheduler.yield() 时,scheduler.yield() 调用也会继承 Window.requestIdleCallback() 的优先级,即继承 background 优先级值。
但需要注意的是:在 requestIdleCallback() 回调函数内部调用 scheduler.yield() 是 不可中止 的。
总结
Scheduler.yield() 凭借其能够拆分长任务、并在相同优先级队列中享有更高执行顺序的特性,成为了优化前端任务调度与响应速度的利器。它与 Scheduler.postTask() 各有侧重,yield() 更擅长于从内部优化现有任务的执行流,而 postTask() 则提供了更丰富的任务编排能力(如延迟、显式优先级控制与中止)。理解两者的机制与适用场景,能帮助开发者更精准地进行性能调优。
参考资料
本文由云栈社区整理发布,希望对你有所帮助。