代表性面试主题

如何安全使用 scheduler.postTask,并为不支持的浏览器设计降级?

前端困难
OfferCC 编辑团队发布 更新

题干

一个 Web 应用需要把搜索索引、可见区域渲染和低优先级预取放到主线程后台处理。请用 scheduler.postTask 设计调度器,并说明任务优先级、取消、scheduler.yield、浏览器不支持时的降级和如何证明交互没有变慢。

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. 参考实现

javascript
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 密集工作。

公开来源

同类题目

资源

产品对比

每份对比均基于官方资料,并标注复核日期。

查看完整对比