async/await和forEach

async/await使用起来非常棒,但是在Array.forEach()中却存在陷阱。

问题描述

例如说下面的代码:

const waitFor = (ms) => new Promise(r => setTimeout(r, ms));
[1, 2, 3].forEach(async (num) => {
  await waitFor(1000);
  console.log(num);
});
console.log('Done');

在控制台执行上面代码结果如下(node.js版本≥ 13.0.0):

$ node forEach.js
$ Done
$ 1
$ 2
$ 3

问题分析

console.log(num)的结果在最后才输出,并且是同时输出。

我们创建一个forEach()方法理解下到底发生了什么事:

Array.prototype.forEach = function (callback) {
  // 这里迭代我们的数组
  for (let index = 0; index < this.length; index++) {
    // 每次迭代执行 callback 方法,传入对应参数
    callback(this[index], index, this);
  }
};

forEach() 的 polyfill 实现可以参考:MDN-Array.prototype.forEach()

可以看到,callback并没有在async/await中执行,所以Promise的结果在最后才打印。

解决问题

我们可以用自己写的asyncForEach方法代替forEach

asyncForEach([1, 2, 3], async (num) => {
  await waitFor(50);
  console.log(num);
})
console.log('Done');

执行上面的代码,可以看到下面的结果:

$ node forEach.js
$ Done
$ 1
$ 2
$ 3

结果好像没什么区别,不过console.log(num)已经在async/await中执行了,我们也可以看到setTimeout的效果。

但是还不够理想,实际上asyncForEach返回的是一个Promise,因为它包裹在一个async函数中,我们可以等待它执行完成后再打印出最后的Done

const start = async () => {
  await asyncForEach([1, 2, 3], async (num) => {
    await waitFor(50);
    console.log(num);
  });
  console.log('Done');
}
start();

再次执行可以看到正确的结果:

$ node forEach.js
$ 1
$ 2
$ 3
$ Done