异步函数 | forEach遇上await

forEach和await我们都不陌生

其中,forEach是JS中Array的API,用于遍历数组中的元素,并且可以传入一个回调函数去修改数组中的每一项,特点是没有返回值【不能用return,也不能用break跳出循环】,且会修改原数组

await是异步函数async/await发挥主要作用的标识符,用于阻塞代码的执行,实现异步效果

那么,forEach遇上await会擦出怎样的火花呢?

问题描述

看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function getNumber() {
return Promise.resolve([1,2,3]);

// 返回结果为一个Promise实例
/*
Promise {<fulfilled>: [1,2,3]};
*/
}


function multi(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num) {
resolve(num * num);
} else {
reject(new Error('num not specified'));
}
}, 1000);
})
}


async function test() {
let nums = await getNumber(); // await解包:如果表达式是 promise 对象, await 返回的是 promise 成功的值
// nums是一个数组[1,2,3]
nums.forEach(async item => {
let res = await multi(item);
console.log(res);
})
}

test();

我们期望的打印结果是:

1
2
3
4
5
6
// 1秒后
// 1
// 1秒后
// 4
// 1秒后
// 9

实际的打印结果是:

1
2
3
4
// 1秒后
// 1
// 4
// 9

说明,forEach里面的回调函数并没有异步执行,而是同步执行的

分析问题

在上面这个例子中,我们给forEach的回调函数套上了async/await,使其具有异步函数的功能,我们期望其可以串行执行函数,但实际却是并行执行了

forEach 的 polyfill 参考:MDN-Array.prototype.forEach(),可以帮助我们理解:

1
2
3
4
5
6
7
Array.prototype.forEach = function(cb) {
// this指代调用forEach的array
for (let index = 0; i < this.length; i++) {
// 回调函数的三个参数:index,item,array
cb(index, this[index], this);
}
}

从上面的代码可以得知,forEach只是简单地执行了回调函数而已,而不会去处理异步的情况

test函数可以修改为等价的代码,如下所示:

1
2
3
4
5
6
7
8
9
async function test() {
let nums = await getNumber(); // nums为 [1,2,3]

for (let i = 0; i < nums.length; i++) {
(async item => {
let res = await multi(item);
console.log(res);
})(nums[i]);
}

从以上代码可以得知:forEach中异步函数是并行执行,导致了一次性全部输出结果:1,4,9

解决办法

方式一:for循环

通过改造forEach,确保每一个异步的回调执行之后再去执行下一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function asyncForEach(array, cb) {
for (let i = 0; i < array.length; i++) {
await cb(array[i], i, array);
}
}

async function test() {
let nums = await getNumber(); // nums为 [1,2,3]

asyncForEach(nums, async item => {
let res = await multi(item);
console.log(res);
})
}

方式二:for…of

for…of用于遍历可迭代对象,先调用可迭代对象的迭代器方法[Symbol.iterator]()方法,该方法返回一个迭代器对象,对象内部包含next()方法,然后调用迭代器对象上的next方法

1
2
3
4
5
6
7
8
async function test() {
let nums = await getNumber(); // nums为 [1,2,3]

for (let num of nums) {
let res = await multi(num);
console.log(res);
}
}

上述代码等价于下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
async function test() {
let nums = await getNumber(); // nums为 [1,2,3]
let iterator = nums[Symbol.iterator]();
let objIterator = iterator.next(); // {value: xxx, done: true || false}

while (!objIterator.done) {
let num = objIterator.value;
let res = await multi(num);
console.log(res);
objIterator = iterator.next();
}
}

参考

当 async/await 遇上 forEach

MDN-Array.prototype.forEach()