当async/await遇上forEach

 

当async/await遇到forEach会有什么反应呢? 这篇文章为你揭晓。

当async/await遇上forEach

前情提要

这是在做格式化wang.oa.com的时候遇到的一个问题,在邮件中提出后,收到了avenwuerasermeng两位前辈的回复和指导,特此感谢。本文在他们指导后,经我整理后完成。

avenwu:

for和forEach的差别是后者不能正常的跳出循环(return、break等),其它的差别不大,把forEach转成for的写法就知道为什么你的for写法可以顺序执行而forEach不能:

for:

const report = async () => {
    for (let i = 0, len = arr.length; i < len; i++) {
        await asyncFn(arr[i]);
    }
};

forEach:

const report = async () => {
    arr.forEach(async (item) => {
        await asyncFn(item);
    });
};

上述forEach的写法相当于:

const report = async () => {
    const asyncFnWrap = async (item) => {
      await asyncFn(item);
    };
    for (let i = 0, len = arr.length; i < len; i++) {
        asyncFnWrap(arr[i], i);
    }
};

第一个for循环的 asyncFn 要wait返回后才继续执行,所以是顺序执行,而第二个的 asyncFnWrap 不会阻塞循环。

erasermeng:

实际上还是forEach内部实现不支持await的问题(模拟下):

Array.prototype.forEach = function (callback) {
  // this represents our array
  for (let index = 0; index < this.length; index++) {
    // We call the callback for each entry
    callback(this[index], index, this)
  }
}

来个扩展支持版的,哈哈:

Array.prototype.forEachAsync = async function (fn) {
    for (let t of this) { await fn(t) }
}

Array.prototype.forEachAsyncParallel = async function (fn) {
    await Promise.all(this.map(fn));
}

问题提出

forEach遇到async/await会发生什么?

JavaScript中的循环数组遍历

在 JavaScript中提供了如下四种循环遍历数组元素的方式:

  • for 这是循环遍历数组元素最简单的方式
for(i = 0; i < arr.length; i++) {
    console.log(arr[i]);
  }
  • for-in for-in 语句以任意顺序遍历一个对象的可枚举属性,对于数组即是数组下标,对于对象即是对象的 key 值。注意 for-in 遍历返回的对象属性都是字符串类型,即使是数组下标,也是字符串 “0”, “1”, “2” 等等。[不推荐使用 for-in 语句]
for (var index in myArray) {
    console.log(myArray[index]);
  }
  • forEach forEach 方法用于调用数组的每个元素,并将元素传递给回调函数;注意在回调函数中无法使用 break 跳出当前循环,也无法使用 return 返回值
myArray.forEach(function (value) {
    console.log(value);
  });
  • for-of for-of 语句为各种 collection 集合对象专门定制的,遍历集合对象的属性值,注意和 for-in 的区别
for (var value of myArray) {
    console.log(value);
  }

分析问题

在本例中 forEach 的回调函数是一个异步函数,异步函数中包含一个 await 等待 Promise 返回结果,我们期望数组元素串行执行这个异步操作,但是实际却是并行执行了。

forEach 的 polyfill 参考:MDN-Array.prototype.forEach(),简单点理解:

Array.prototype.forEach = function (callback) {
  // this represents our array
  for (let index = 0; index < this.length; index++) {
    // We call the callback for each entry
    callback(this[index], index, this)
  }
}

相当于 for 循环执行了这个异步函数,所以是并行执行

解决问题

方式一

我们可以改造一下 forEach,确保每一个异步的回调执行完成后,才执行下一个

async function asyncForEach(array, callback) {
  for (let index = 0; index < array.length; index++) {
    await callback(array[index], index, array)
  }
}

async function test () {
  var nums = await getNumbers()
  asyncForEach(nums, async x => {
    var res = await multi(x)
    console.log(res)
  })
}

方式二

使用 for-of 替代 for-each

for-of 可以遍历各种集合对象的属性值,要求被遍历的对象需要实现迭代器 (iterator) 方法,例如 myObject[Symbol.iterator]() 用于告知 JS 引擎如何遍历该对象。一个拥有 [Symbol.iterator]() 方法的对象被认为是可遍历的。

var zeroesForeverIterator = {
  [Symbol.iterator]: function () {
    return this;
  },
  next: function () {
    return {done: false, value: 0};
  }
};

如上就是一个最简单的迭代器对象;for-of 遍历对象时,先调用遍历对象的迭代器方法 [Symbol.iterator](),该方法返回一个迭代器对象(迭代器对象中包含一个 next 方法);然后调用该迭代器对象上的 next 方法。

每次调用 next 方法都返回一个对象,其中 done 和 value 属性用来表示遍历是否结束和当前遍历的属性值,当 done 的值为 true 时,遍历就停止了。

for (VAR of ITERABLE) {
  STATEMENTS
}

等价于:

var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
  VAR = $result.value;
  STATEMENTS
  $result = $iterator.next();
}

由此可以知道 for-of 和 forEach 遍历元素时处理的方式是不同的。使用 for-of 替代 for-each 后代码为:

async function test () {
  var nums = await getNumbers()
  for(let x of nums) {
    var res = await multi(x)
    console.log(res)
  }
}