← 返回题目列表

如何手写一个简单的任务切片调度器?

困难 第 27 / 27 题 更新于 2026/07/29
手写代码调度器性能优化任务切片

简化版

任务切片调度器用于把大量同步任务拆成小块执行,避免长时间阻塞主线程。实现思路是维护任务队列,每一轮只在时间预算内执行一部分任务,剩余任务放到下一轮继续。调度可以用 requestIdleCallbackMessageChannelsetTimeout。边界包括任务耗时不可控、优先级、取消、错误隔离和浏览器兼容。

详细版

基础版可以用时间片控制。

组成作用
queue保存待执行任务
schedule安排下一轮执行
deadline控制本轮执行多久
error handling单个任务失败不拖垮队列
function createScheduler(timeSlice = 5) {
  const queue: Array<() => void> = []
  let scheduled = false

  function flush() {
    scheduled = false
    const start = performance.now()
    while (queue.length && performance.now() - start < timeSlice) {
      queue.shift()?.()
    }
    if (queue.length) schedule()
  }

  function schedule() {
    if (!scheduled) {
      scheduled = true
      setTimeout(flush, 0)
    }
  }

  return { add(task: () => void) { queue.push(task); schedule() } }
}

切片调度优化的是“不要连续占用主线程”,不是让总计算量消失。

完整版教学

一、为什么需要任务切片

如果一次性处理十万条数据、渲染大量节点或做复杂计算,主线程会长时间被占用。用户点击、滚动和渲染都要排队,页面看起来就卡住了。

任务切片的目标是把一个长任务拆成多个短任务,中间把控制权还给浏览器。

二、时间片模型

调度器给每一轮执行设置预算,例如 5ms。超过预算就暂停,下一轮再继续。

这和 React Concurrent Rendering 的理念有点相似:重要任务优先,不重要任务可中断。

三、基础队列实现

最简单模型是 FIFO 队列。

const scheduler = createScheduler(8)
items.forEach(item => {
  scheduler.add(() => processItem(item))
})

这样不会一次性处理所有 item,而是分多轮执行。

四、调度 API 怎么选

不同 API 有不同特点。

API特点适合场景
setTimeout简单,延迟不稳定面试基础版
MessageChannel比 setTimeout 更快进入宏任务框架调度
requestIdleCallback利用空闲时间低优先级任务
requestAnimationFrame绘制前执行视觉更新

面试手写用 setTimeout 可以,说明可替换为更合适调度源即可。

五、错误隔离

如果一个任务抛错,不能让整个队列停止。可以 try/catch 单个任务。

try {
  task()
} catch (err) {
  console.error(err)
}

是否继续执行要看业务,但调度器通常要具备错误隔离能力。

六、取消和优先级

工程版调度器通常支持:

  • 返回任务 id。
  • 按 id 取消任务。
  • 高低优先级队列。
  • 超时任务强制执行。

这些能力会让实现复杂很多。面试里可以先写基础版,再讲扩展方向。

七、常见误区与追问

  • 误区:任务切片能减少总耗时。 它主要减少连续阻塞,让页面更可交互,总计算量通常不变。
  • 误区:时间片越小越好。 太小会增加调度开销,太大又会形成长任务,需要折中。
  • 误区:所有任务都适合切片。 强依赖同步结果的任务、事务性任务不适合随便拆。
  • 追问:为什么不用微任务调度? 微任务会在渲染前持续清空,容易继续饿死渲染。
  • 追问:如何支持优先级? 使用多个队列,每轮先取高优先级,再取低优先级。
  • 追问:重计算用切片还是 Worker? 如果计算很重且可脱离 DOM,Worker 更合适;切片适合主线程轻量分批。

八、加强记忆

任务切片记成“长任务切短,中间让路”。队列保存任务,时间片控制执行,调度源安排下一轮;扩展点是错误、取消和优先级。