题目
逛掘金发现一个题目,很有意思
//JS实现一个带并发限制的异步调度器Scheduler,保证同时运行的任务最多有两个。完善代码中Scheduler类,使得以下程序能正确输出
class Scheduler {
add(promiseCreator) { ... }
// ...
}
const timeout = (time) => new Promise(resolve => {
setTimeout(resolve, time)
})
const scheduler = new Scheduler()
const addTask = (time, order) => {
scheduler.add(() => timeout(time))
.then(() => console.log(order))
}
addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')
// output: 2 3 1 4
// 一开始,1、2两个任务进入队列
// 500ms时,2完成,输出2,任务3进队
// 800ms时,3完成,输出3,任务4进队
// 1000ms时,1完成,输出1
// 1200ms时,4完成,输出4
解答
评论区大神createAny给了一个解法,之前对async函数的理解不够,没看懂,断点调了一下才搞清楚,加深了对async函数的理解
class Scheduler {
constructor() {
this.awaitArr = [];
this.count = 0;
}
async add(promiseCreator) {
this.count >= 2 ? await new Promise(resolve => this.awaitArr.push(resolve)) : '';
this.count++;
const res = await promiseCreator();
this.count--;
this.awaitArr.length && this.awaitArr.shift()();
return res;
}
}
分析
为了搞清楚程序的流程,我添加了一些打印信息,代码如下。
要不要试试手写一下打印结果?如果写的完全正确,说明你对async函数的流程真的掌握的很清楚了。
但是菜鸡如我,还是一步一步分析吧
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<script>
let i = 0;
class Scheduler {
constructor() {
this.awaitArr = [];
this.count = 0;
}
async add(promiseCreator) {
console.log("start scheduler.add");
let resultOfFirstAwait
if (this.count >= 2) {
console.log("this.count >= 2");
resultOfFirstAwait = await new Promise(resolve => {
this.awaitArr.push(() => {
console.log("before resolve");
resolve(console.log(`i = ${i}`) || i++);
console.log("after resolve");
});
});
}
console.log(`resultOfFirstAwait = ${resultOfFirstAwait}`);
this.count++;
const res = await promiseCreator();
console.log("=====================");
console.log(performance.now());
this.count--;
this.awaitArr.length && this.awaitArr.shift()();
console.log("before return scheduler.add");
return res;
}
}
const timeout = time => new Promise(resolve => setTimeout(() => resolve(time), time))
const scheduler = new Scheduler();
const addTask = (time, order) => {
console.log("-------------------");
console.log("start addTask");
const p = scheduler.add(() => {
console.log("calling promiseCreator");
return timeout(time);
});
console.log("got promise from scheduler.add");
p.then(res => {
console.log(
`res = ${res}, order = ${order}, now = ${performance.now()}`
);
});
console.log("end addTask");
};
addTask(10000, "1");
addTask(5000, "2");
addTask(3000, "3");
addTask(4000, "4");
</script>
</body>
</html>
流程分析
- 62行
- 开始执行addTask,打印47,48行
- 开始执行scheduler.add,打印15行
- this.count === 0,走到28行,此时resultOfFirstAwait === undefined,打印28行
- 走到30行,this.count变为1
- 走到31行,await promiseCreator()
- 调用promiseCreator,打印50行
- promiseCreator返回的是一个promise,这个promise是timeout函数的返回值,是一个10s之后将10给resolve的promise
- 由于31行的await语句后面的promise当前处于pending状态,JS将继续执行同步代码
- 这个时候来到了我开始没搞清楚的地方。下一句打印的是53行。虽然scheduler.add没有走到return,但是49行的p已经被赋值了一个promise。
- 这个promise不是scheduler.add里await或return的任何promise,而是JS内部生成的一个promise。
- async函数一旦遇到了第一个await或return就会退出,返回一个内部生成的promise
- 这个promise会把async函数最后return的东西给resolve了。也就是说,第54行的res是async函数最后return的东西。如果async函数return的是一个promise,那么第54行的res是这个promise给resolve了的结果。
- 下面走到54行,给通过scheduler.add这个async函数得到的promise添加一个.then,添加一个微任务。
- 打印59行,第一个addTask函数执行完(addTask是一个同步函数)
- 走到63行,执行第二个addTask
- 类似上面的,依次打印 47, 48, 15, 28, 50, 53, 59行。第二个addTask函数执行完
- 走到64行,执行第三个addTask
- 打印47,48,15行
- 这个时候this.count === 2,打印18行
- 调用await后的函数,我们知道new其实是一个语法糖,是同步代码,所以会立即执行。this.awaitArr给push了第1个函数
- 像上面说的那样,”async函数一旦遇到了第一个await或return就会退出,返回一个内部生成的promise“,所以代码走到49行,p现在是一个promise了
- 打印53行
- 给这个promise添加一个.then
- 打印59行,第三个addTask执行完
- 走到65行,执行第四个addTask
- 类似的,依次打印47, 48, 15, 18, 53, 59行
上面是同步代码,也就是4个addTask函数的执行部分
下面执行异步代码。
5s之后......
- 63行的addTask对应的promiseCreator被resolve了
- 走到31行,res被赋值。res现在是什么呢?
- 我们知道,await后面跟的是一个promise,res就是这个promise给resolve了的结果
- 这个promise是什么呢?是promiseCreator的返回结果,也就是第43行的timeout的返回值,也就是43行new的那个promise
- 这个promise给传进来的time给resolve了,所以res就是这个time,也就是5000
- 打印33,34行
- 走到35行,this.count === 1
- 走到37行,this.awairArr的队首弹出队列,执行之。
- 走到21行,打印21行
- 走到22行,执行resolve函数。
- 这个resolve函数是JS内置的函数
- 首先调用
console.log(i = ${i}),打印22行,因为console.log的返回值是undefined,所以resolve的是i,当前为0 - 问题来了,有2个问题
- 第一,这个时候已经调用resolve了,下面的23行还会打印吗?
- 第二,假如23不执行,那么下一步打印什么?假如23行执行,那么执行23行以后打印什么?
- 现在回答第一个问题:
- 会打印23行。调用resolve以后,promise不会立即被resolve,而是继续执行剩下的同步代码
- 回答第二个问题,23行代码执行晚一会,执行什么?
- 也许你的想法是:promise被resolve了,那么第19行的await就应该有结果了,所以会给19行的resultOfFirstAwait赋值,所以打印28行
- 然鹅真实情况是:打印38行
- 为了叙述方便,第63行的addTask对应的调用栈称为A栈,第64行的addTask对应的调用栈称为B栈。可以看出,调用resolve函数虽然会立即把promise的状态置位resolved,但是因为当前A栈还有代码为执行,所以会继续执行A栈的代码,不会立即跳到B栈。
- 38行执行完以后,下面打印什么呢?
- 这个问题在node和浏览器环境下表现不一样。
- node环境下,会先打印55行的代码,然后再打印28行
- 浏览器环境下,会先打印28行代码,再打印55行
- 我认为这是系统实现事件循环的方式不一样。node应该是把resolve以后的调用栈放到下一个任务队列的队首,而浏览器是放到当前任务队列的末尾
- 所以在浏览器环境下,打印28行,B栈
- 接下来调用promiseCreator,打印50行,B栈
- 打印55行,A栈
3s之后...
类似上面的执行过程,
- 64行的addTask对应的promiseCreator被resolve了
- 打印33, 34, 21, 22, 23, 38, 28, 50, 55行
2s之后...
- 61行的addTask对应的promiseCreator被resolve了
- 打印33, 34行
- 现在this.awaitArr空了,所以不会执行this.awaitArr.shift()()
- 打印38行
- 打印55行
2s之后...
- 65行的addTask对应的promiseCreator被resolve了
- 打印33, 34, 38, 55行
结束
小结
- async函数一旦遇到了第一个await或return就会退出,返回一个内部生成的promise
- 调用promise的resolve函数会立即把promise的状态置为resolved,但不会立刻跳转调用栈。并且node和浏览器的实现方式有差异