1. 题目
页面会在用户输入时更新搜索结果,同时还要解析离线索引并预取下一页。请设计一个基于 Prioritized Task Scheduling API 的任务调度层:用户可见更新应及时执行,预取不能长期阻塞输入;说明 scheduler.postTask()、scheduler.yield()、取消信号和不支持该 API 时的行为。
2. 约束与澄清
- 所有任务运行在同一个窗口或 Worker 的事件循环中,长同步函数仍会阻塞当前线程。
- 任务至少分为
user-blocking、user-visible和background三种优先级。 - 任务可能在滚动、路由切换或新输入后失去价值,需要可取消或降低优先级。
- 必须保留可运行的降级路径,不能把浏览器支持当成业务正确性的前提。
3. 核心思路
scheduler.postTask(callback, options) 把回调排入带优先级的任务队列,并返回一个 Promise;priority 可以是 user-blocking、user-visible 或 background。需要取消时传入 AbortSignal;共享 TaskController 还可以改变尚未执行任务的优先级。scheduler.yield() 让异步函数主动把控制权交还给浏览器,再继续后半段工作。
优先级只影响调度顺序,不会抢占已经运行的 JavaScript。每个回调仍应切成短片段,并在批次之间让出控制权。任务调度 API 不可用时,可采用小批量 setTimeout、MessageChannel 或现有框架调度器,并保持相同的取消与过期语义。
4. 参考实现
const scheduler = globalThis.scheduler;
function scheduleWork(task, priority, signal) {
if (scheduler?.postTask) {
return scheduler.postTask(task, { priority, signal });
}
return new Promise((resolve, reject) => {
const run = () => {
if (signal?.aborted) {
reject(signal.reason);
return;
}
Promise.resolve().then(task).then(resolve, reject);
};
setTimeout(run, priority === "background" ? 50 : 0);
});
}
async function indexInChunks(items, signal) {
for (let i = 0; i < items.length; i += 100) {
await scheduleWork(() => buildIndex(items.slice(i, i + 100)),
"background", signal);
if (scheduler?.yield && i + 100 < items.length) {
await scheduler.yield({ signal });
}
}
}5. 性能与正确性
任务优先级不会改变代码的执行结果,也不会打断一个正在运行的回调;它只能影响尚未开始任务的相对顺序。scheduler.postTask() 的返回 Promise 会在回调返回值完成时解决,回调抛错或信号取消时应让调用方处理 rejection。
真正的性能边界是任务时长和总工作量:把 200 毫秒的同步循环放进低优先级队列仍会阻塞输入。应按批次测量 Long Tasks、输入延迟和取消命中率,调整批大小;CPU 密集且可并行的工作可考虑 Worker,不能只靠优先级掩盖主线程饥饿。
6. 追问与陷阱
- 检测
globalThis.scheduler?.postTask,不要只根据浏览器品牌或版本猜支持情况。 AbortSignal只能取消尚未开始或能观察信号的工作;已经执行的同步回调不会被强制中断。- 动态改变优先级不等于抢占;若任务已经出队,仍需在业务层设置过期版本,避免写入过时结果。
- 降级到
setTimeout不能复现原生优先级的全部语义,必须验证用户可见任务、后台任务和取消路径仍能工作。
7. 延伸阅读
可以比较 scheduler.postTask() 与 requestIdleCallback()、MessageChannel 和框架调度器:前者提供显式优先级与取消,空闲回调依赖空闲时机,消息通道只提供排队,框架调度器还可能加入组件生命周期语义。选型时应结合兼容性、任务类型和测量结果。
8. 面试评分点
能解释优先级边界
应说明三种优先级、Promise 返回值和“只排序未开始任务”的语义,不能把它描述成线程抢占。
能设计可取消任务
应使用 AbortSignal 或 TaskController,并讨论已开始回调无法强制终止、结果版本需要过期检查。
能写出渐进式降级
应先做能力检测,再提供定时器、消息通道或框架调度器降级,同时保留相同的任务分级和取消行为。
能用数据证明收益
应测量 Long Tasks、输入延迟、批次时长和取消命中率,并指出 Worker 才能隔离真正的 CPU 密集工作。