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

3,462 阅读3分钟

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

题目

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

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

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

我: ... 卒

挑战

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

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秒,并在之后打印对应输出

wait 3s // 等待3s

execute
wait 4s // 等待4s

execute
wait 5s // 等待5s

execute

方式1. reduce

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

比较简单和常见的方式

方式2. async + 循环 + await

(
  async function () {
    for (const v of arr) {
      await delay(v)
    }
  }
)()

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

方式3. 普通循环

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

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

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

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

let i
let p = Promise.resolve()
while (i = arr.shift()) {
  p = p.then(() => delay(i))
}

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

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

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

let i
let p = Promise.resolve()
while (i = arr.shift()) {
  let s = i
  p = p.then(() => delay(s))
}

方式4. 递归

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

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]方法即可

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循环即可

方式6. generator

function* gen() {
  for (const v of arr) {
    yield delay(v)
  }
}

function run(gen) {
  const g = gen()

  function next(data) {
    const result = g.next(data)
    if (result.done) return result.value
    result.value.then(function(data) {
      next(data)
    })
  }

  next()
}

run(gen)

先创建一个generator函数,然后再封装一个自执行run函数

后记

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

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

与君共勉!