最近看到一道手写代码题:js并发任务调度实现,最大并发数为2
;顺手实现了一下,分享自己的解题思路。
先看题目:
function timeout(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, time)
})
}
function addTask(time, name) {
superTask
.add(() => timeout(time))
.then(() => {
console.log(`任务${name}完成`)
})
}
const superTask = new SuperTask()
addTask(10000, 1) // 10000ms后输出 任务1完成
addTask(5000, 2) // 5000ms后输出 任务2完成
addTask(3000, 3) // 8000ms后输出 任务3完成
addTask(4000, 4) // 12000ms后输出 任务4完成
addTask(5000, 5) // 15000ms后输出 任务5完成
拿到题目先别动手,来拆解一下已知信息:
- 首先声明了两个函数,一个timeout函数,一个addTask函数;以及一个基于SuperTask构造出来的实例;
- timeout函数接收一个参数,返回promise,接收的参数传给
setTimeOut
用于定义该promise内部何时执行resolve,所以,timeout用于构建一个用于自定义resolve的promise; - addTask函数接收两个参数,
time
和name
,首先调用superTask实例.add,传入一个箭头函数,箭头函数内部将参数time
传入timeOut函数并执行,接着调用了.then(推导出.add返回一个Promise实例),最终打印传入的参数name
; - 最后执行输出:
2-3-1-4-5
分析之后,得出结论,我们需要实现SuperTask类,并且它具有一个返回promise的add方法,同时在add方法内,需要用一个队列收集添加的执行函数,同时要确保执行的最大并发数。
梳理执行逻辑:
于是我们拿到单次任务调度的执行过程:
由于每次任务调度都执行了.add方法,于是我们知道,.add接管了所有任务调度的执行:
实现过程:
- 实现任务的收集
class SuperTask {
constructor() {
this.taskList = [] // 任务队列
this.excuteNum = 0 // 当前执行任务数
this.taskExcuteMax = 2 // 最大任务调度数
}
/**
* @param: 返回值为promise的函数
* @return 返回promise
*/
add(promiseCallback) {
return new Promise((resolve) => {
this.taskList.push({
executeFn: promiseCallback,
callbackFn: resolve,
})
})
}
}
此时打印superTask.taskList,我们发现,通过superTask.add的任务已经都被收集了,包括timeout箭头函数,以及.add返回的promise的resolve回调:
- 支持调度任务批次执行
class SuperTask {
constructor() {
this.taskList = [] // 任务队列
this.excuteNum = 0 // 当前执行任务数
this.taskExcuteMax = 2 // 最大任务调度数
this.excuteItv = null
this.run() // 初始化时执行一次调度器
}
/**
* @param: 返回值为promise的函数
* @return 返回promise
*/
add(promiseCallback) {
return new Promise((resolve) => {
this.taskList.push({
executeFn: promiseCallback,
resolveFn: resolve,
})
this.run()
})
}
run() {
this.excuteItv = setInterval(() => {
if (this.taskList && this.excuteNum < this.taskExcuteMax) {
const executeTask = this.taskList.shift() // 从队头开始取调度函数执行
this.excuteNum++
executeTask.executeFn().then(() => {
executeTask.resolveFn()
this.excuteNum--
})
}
if (this.taskList.length === 0) {
clearInterval(this.excuteItv)
}
}, 0)
}
}
测试一下我们的功能代码:
可以,通过,如果想更清晰的知道执行的时间,我们可以加以下代码:
let timer = 0
let globalTimer = setInterval(() => {
timer += 1
console.log(timer)
if (timer > 16) {
clearInterval(globalTimer)
}
}, 1000)
addTask(10000, 1) // 10000ms后输出 任务1完成
addTask(5000, 2) // 5000ms后输出 任务2完成
addTask(3000, 3) // 8000ms后输出 任务3完成
addTask(4000, 4) // 11000ms后输出 任务4完成
addTask(5000, 5) // 15000ms后输出 任务5完成
完美验证。
思考:while与setInterval
由于看到有网友用while执行调度,那么用while的方式处理对比setInterval如何呢,由于while是同步阻塞执行,是否会影响后面定时器函数的调用精度?
带着这样的思考,我又用while实现了一下任务调度逻辑:
// 增加whileRun函数
whileRun() {
while (this.taskList.length && this.excuteNum < this.taskExcuteMax) {
const executeTask = this.taskList.shift()
this.excuteNum++
executeTask.executeFn().then(() => {
executeTask.resolveFn()
this.excuteNum--
})
}
}
调用了一下打印结果,相比之下无差别:
也许是代码复杂度不够高,目前看不出这两者的差异; 不过相比while的执行方式,setInterval可以更加灵活自由的决定其run函数的调用位置,而while则要保证是在每次push-taskList之后调用.