面试官: 来说一下如何串行执行多个 Promise

最近接到了一次阿里的电话面试,问的问题都挺有意思的,而且看重的不仅仅是问题能否回答得上来,还得能明白背后的原理以及能否使用其他的方式实现

其中一个题很有印象,也很常见,题目: 如何串行执行多个 Promise

面试官: 来说一下如何串行执行多个 Promise

最近接到了一次阿里的电话面试,问的问题都挺有意思的,而且看重的不仅仅是问题能否回答得上来,还得能明白背后的原理以及能否使用其他的方式实现

题目

其中一个题很有印象,也很常见,题目: 如何串行执行多个 Promise。

这要换在平时,经常看些面经或写点代码也能答出来: 使用Array.prototype.reduce、使用async + 循环 + await、 或者使用新出的for await of

面试官: 那你还能说出使用其他的方式来实现吗? for await of 的规则如何?

我: … 卒

挑战

好吧,我们今天的挑战就是用各种方式来实现这个需求, 为此,我准备了一段代码

1
2
3
4
5
6
7
8
9
10
11
function delay(time) {
return new Promise((resolve, reject) => {
console.log(`wait ${time}s`)
setTimeout(() => {
console.log('execute')
resolve()
}, time * 1000)
})
}

const arr = [3, 4, 5]

一个封装的延迟函数,然后一个装有 3,4,5 的数组,需求就是在开始执行时依次等待 3, 4, 5 秒,并在之后打印对应输出

1
2
3
4
5
6
7
8
9
wait 3s // 等待3s

execute
wait 4s // 等待4s

execute
wait 5s // 等待5s

execute

方式 1. reduce

1
2
3
arr.reduce((s, v) => {
return s.then(() => delay(v))
}, Promise.resolve())

比较简单和常见的方式

方式 2. async + 循环 + await

1
2
3
4
5
;(async function() {
for (const v of arr) {
await delay(v)
}
})()

本质上使用了 async/await 的功能

方式 3. 普通循环

其实仔细想想方式 1 的本质是使用一个中间变量(上一次执行结果)来保存链式Promise, 那我们举一反三, 换别的循环也可以实现

1
2
3
4
let p = Promise.resolve()
for (const i of arr) {
p = p.then(() => delay(i))
}

理论上所有循环方式都能实现,只要找到一个保存链式Promise的地方,闭包也好,参数也好。

其实使用 while 循环时遇到一些坑,例如这样写。

1
2
3
4
5
let i
let p = Promise.resolve()
while ((i = arr.shift())) {
p = p.then(() => delay(i))
}

思路没啥问题,问题就在于 i 放在外层时实际上每次都被改动,这和一道经典的面试题一样

1
2
3
4
5
for(var i = 0, i < 5, i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}

事实上他们都会输出 5。所以对于 while 循环,我们需要在内部也保存一份,最后我改成这样,感觉有点蠢,不过也没想到其他办法

1
2
3
4
5
6
let i
let p = Promise.resolve()
while ((i = arr.shift())) {
let s = i
p = p.then(() => delay(s))
}

方式 4. 递归

这是面试官提供的思路,也提到了koa,其实koa自己也有研究,其中洋葱模型来自于koa-compose库。

1
2
3
4
5
function dispatch(i, p = Promise.resolve()) {
if (!arr[i]) return Promise.resolve()
return p.then(() => dispatch(i + 1, delay(arr[i])))
}
dispatch(0)

方式 5. for await of

通过查阅了for await of的规则,其实for await offor of规则类似,只需要实现一个内部[Symbol.asyncIterator]方法即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function createAsyncIterable(arr) {
return {
[Symbol.asyncIterator]() {
return {
i: 0,
next() {
if (this.i < arr.length) {
return delay(arr[this.i]).then(() => ({
value: this.i++,
done: false
}))
}

return Promise.resolve({ done: true })
}
}
}
}
}

;(async function() {
for await (i of createAsyncIterable(arr)) {
}
})()

先创建出一个可异步迭代对象,然后丢到for await of循环即可

方式 5. generator + co

因为平时对于generator只是看过文档没实际使用,所以对于这个类还不太熟,面试官也提了可以看看 co 的实现,所以这里的内容先欠着,后续再补上 囧,逃~

后记

一个合格的工程师: 能找到或写出市面上最主流的实现方式

一个出色的工程师: 能明白其中的原理,并能举一反三,有自己的思考。

与君共勉!