这个问题面试的时候很常见,10
次面试可能会遇到 8
次。
"写个异步调度器?" - 这题简单,我熟!
"那再加个暂停功能?" - 嗯...也行!
"取消功能呢?" - 还可以搞定...
"错误重试和事件通知呢?" - 😅 面试官,你是不是想招架构师啊...
虽然基础的异步调度器实现起来不难,但面试官往往会步步深入,考察你对异步任务控制的理解深度。
最近经常被面试官问到了这个问题,写这篇文章记录一下。毕竟现在的面试官都喜欢问这种"看似简单实则复杂"的问题 😭
🤖 需求分析
先别慌,我们一步步来,先看最基本的需求:
- 支持并发的异步调度器,最多允许2两任务进行处理
- 最后输出结果
2 3 1 4
看起来很简单对吧?就是控制一下并发数,然后按顺序执行任务...
等等!面试官的笑容逐渐诡异了起来...
const timeout = (time) =>
new Promise((resolve) => {
setTimeout(resolve, time)
})
const scheduler = new Scheduler(2)
const addTask = (time, order) => {
scheduler
.add(() => timeout(time))
.then(() => console.log(order))
.catch((err) => console.log(`任务 ${order} 被取消:`, err))
}
addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')
scheduler.start()
但在实际应用中,面试官往往会继续追问: "如果用户切换页面了呢?" "如果要取消任务呢?" "任务失败了怎么办?" "怎么知道所有任务都完成了?"
好吧...看来一个简单的调度器并不能让面试官满意 😅
scheduler.cancel() // 取消所有任务
scheduler.pause() // 暂停任务执行
scheduler.eventEmitter.on('allTasksFinished', () => {
console.log('所有任务执行完成!')
})
🤖 具体实现
在这里,我们先理解核心原理,再一步步添加新功能。每个功能点都很简单,但组合起来就构成了一个强大的调度器。
面试官:我看你对这个挺熟的啊? 我:那当然...(心虚)
🚀 核心实现
异步调度器的核心就是任务队列和执行控制:
add
函数:返回一个 Promise,这样可以让调用方感知任务执行结果add
函数:构建一个任务推到队列,而不是立即执行run
函数:每个任务执行前检查运行数是否小于并发数,控制并发量run
函数:一个任务执行完,自动从队列取出新任务执行,保持任务连续性start
函数:开始执行队列中的任务,这是整个调度的入口
面试官:嗯,基础的思路不错,那我们来点难的...
🚀 功能点一:怎么取消所有任务?
取消功能需要在几个关键点做处理:
Scheduler
类:加一个标识isCanceled
控制整体状态cancel
函数:将isCanceled
置为true
,切断任务继续执行的可能run
函数:用setTimeout
包裹执行逻辑,让取消有机会在任务执行前生效add
函数:检查isCanceled
,如果已取消就直接 reject
这样可以确保取消及时生效,不会有任务漏执行。
面试官:不错不错,那要是想暂停呢?
🚀 功能点二:怎么暂停和恢复执行任务?
暂停功能相对简单:
Scheduler
类:添加isPaused
标识pause
函数:将isPaused
置为true
run
函数:增加暂停检查,暂停时直接返回不执行任务
这样任务队列会保持当前状态,直到恢复执行。
面试官:(露出了满意的笑容)那如果任务执行失败了呢?
🚀 功能点三:错误重试
在实际应用中,网络请求等异步操作经常会失败,所以需要支持错误重试:
retry
函数:包装原始任务,提供重试次数和延迟时间的控制,确保任务最大程度执行成功。
面试官:最后一个问题,怎么知道所有任务都完成了?
🚀 功能点四:任务完成事件
使用发布订阅模式,让调用方可以监听调度器的状态:
- 初始化时创建
eventEmitter
实例 - 在关键节点触发事件(如所有任务完成时)
- 调用方通过
on
方法注册回调
🤖 完整代码
下面是完整的实现,包含了所有功能点:
class Scheduler {
constructor(limit) {
this.limit = limit
this.runningTasks = 0
this.queue = []
this.isCancelled = false
this.isPaused = false
this.eventEmitter = new EventEmitter()
}
add(promiseMaker, time = 0, delay = 0) {
return new Promise((resolve, reject) => {
const task = async () => {
if (this.isCancelled) {
reject('task has been cancelled')
return
}
try {
const res = await retry(promiseMaker, time, delay)
resolve(res)
} catch (e) {
reject(e)
}
}
this.queue.push(task)
})
}
start() {
this.isCancelled = false
this.isPaused = false
for (let i = 0; i < this.limit && this.runningTasks < this.queue.length; i++) {
this.run()
}
}
run() {
setTimeout(async () => {
if (this.isPaused) return
if (this.queue.length && this.runningTasks < this.limit) {
const task = this.queue.shift()
this.runningTasks++
await task()
this.runningTasks--
this.run()
} else if (this.queue.length === 0 && this.runningTasks === 0) {
this.eventEmitter.emit('allTasksFinished')
}
})
}
cancel() {
this.isCancelled = true
}
pause() {
this.isPaused = true
}
}
function retry(fn, times, delay) {
let _times = times
return new Promise(async (resolve, reject) => {
const run = async () => {
try {
const res = await fn()
resolve(res)
} catch (e) {
if (_times === 0) {
reject(e)
return
}
_times--
setTimeout(fn, delay)
}
}
run()
})
}
🤖 实际应用场景
面试官:说了这么多,给我讲讲实际项目中在哪用过? 我:这个...还真用过!(赶紧搬出例子)
- 文件上传 - 控制并发数,支持暂停/取消
- 数据批量处理 - 分批执行,避免卡顿
- 页面资源加载 - 控制加载优先级
- 后台任务处理 - 错误重试,状态监控
面试官:嗯...还不错,这轮就到这里吧。 我内心:总算过关了 😌
🤖 总结
看似简单的异步调度器,能够延伸出这么多知识点:
- 异步任务控制
- 并发控制
- 错误处理
- 事件通知
所以下次面试被问到类似问题,不要慌,记住:面试官想要的不是完美答案,而是你解决问题的思路
不过现在的面试都卷成这样了吗?一个调度器都要问这么多... 😭
🤖 相关
面试官:听说你简历里写的精通 React 源码,那你给我讲讲 React Scheduler 呗?我:😡 工资加 2K
面试官:你说你做过组件库,肯定了解过复杂组件状态管理的useSyncExternalStore吧?我:😭
版权归许泽川所有
如需转载,请提前询问本人的许可