前言
我在最近项目中遇到了批量申请的一个需求,当时只有单个申请的接口,于是我想到了循环数组请求接口的解决办法,于是就遇上了 async/await 和 循环的问题。我发现在 forEach 中使用 async/await 没有生效,于是在谷歌过程中发现了问题所在,这篇文章讲解的十分详细,案例完整易于理解,是篇不可多得的好文章,于是翻译出来给大家参考,有什么问题大家可以在评论区一起探讨!
噢?你问我最终怎么解决的? 后端同学给了我一个批量申请的接口。

正文
基础的 async 和 await 的使用相对简单,当你试图在循环中使用 await 时,事情就会变得有点复杂了。
案例
举个例子,比方你想知道水果篮 fruitBasket 中的水果数量。
const fruitBasket = {
apple: 27,
grape: 0,
pear: 14
}
你想取得水果篮中每种水果的数量。为了获取它们,你可以定义一个 getNumFruit 函数。
const getNumFruit = fruit => {
return fruitBasket[fruit]
}
const numApples = getNumFruit('apple')
console.log(numApples) // 27
现在,比方说 fruitBasket 位于远程服务器上。访问它需要花费一秒钟。我们可以使用 timeout 定时器来模拟这一秒的延迟。
const sleep = ms => {
return new Promise(resolve => setTimeout(resolve, ms))
}
const getNumFruit = fruit => {
return sleep(1000).then(v => fruitBasket[fruit])
}
getNumFruit('apple')
.then(num => console.log(num)) //27
假设你不想使用 Promise 操作异步任务了,你想使用 async / await 这回调终结者来用同步的方式去执行异步任务,如下:
const control = async _ => {
console.log('Start')
const numApples = await getNumFruit('apple');
console.log(numApples);
const numGrapes = await getNumFruit('grape');
console.log(numGrapes);
const numPears = await getNumFruit('pear');
console.log(numPears);
console.log('End')
}

在 for 循环中使用 Await
假设我们定义一个水果数组。
const fruitsToGet = ['apple', 'grape', 'pear']
循环遍历这个数组
const forLoop = async _ => {
console.log('Start')
for(let index = 0; index < fruitsToGet.length; index++) {
// Get num of each fruit
}
console.log('End')
}
在这个 for 循环中,我们将使用 getNumFruit 来获取并打印每种水果的数量。
因为 getNumFruit 返回一个 promise,我们等待 resolved 结果的返回再打印。
const forLoop = async _ => {
console.log('Start')
for (let index = 0; index < fruitsToGet.length; index ++) {
const fruit = fruitsToGet[index]
const numFruit = await getNumFruit(fruit)
console.log(numFruit)
}
console.log('End')
}
当你使用 await,你可能期望 JavaScript 可以暂停执行直到等到 promise 返回结果。这意味着 await 在一个 for 循环中应该是按顺序执行的的
而结果正是你所期望的:
'Start'
'Apple: 27'
'Grape: 0'
'Pear: 14'
'End'

这种行为在大部分循环中有效(像 while 和 for of循环)...
但是它不能处理需要回调的循环。比如 forEach、map、filter 和 reduce。在接下来几节中,我们将研究 await 如何影响 forEach、map 和 filter。
在 forEach 循环中使用 await
还是上面的示例,首先,先遍历水果数组。
const forEachLoop = _ => {
console.log('Start')
fruitsToGet.forEach(fruit => {
// Send a promise for each fruit
})
console.log('End')
}
然后我们尝试使用 getNumFruit 来获取水果数量。(注意在回调函数中的 async 关键字,我们需要这个 async 因为 await 在回调中)。
const forEachLoop = _ => {
console.log('Start')
fruitsToGet.forEach(async fruit => {
const numFruit = await getNumFruit(fruit)
console.log(numFruit)
})
console.log('End')
}
你大概期望控制台这样打印:
'Start'
'27'
'0'
'14'
'End'
但实际结果不是这样,JavaScript 在 forEach 循环中的 promise 获得结果之前调用了 console.log('End').
'Start'
'End'
'27'
'0'
'14'

其实原因很简单,那就是 forEach 只支持同步代码。
可以参考下 Polyfill 版本的 forEach,简化以后类似就是这样的伪代码。
while (index < arr.length) {
callback(item, index) //也就是我们传入的回调函数
}
从上述代码中我们可以发现,forEach 只是简单的执行了下回调函数而已,并不会去处理异步的情况。 并且你在 callback 中即使使用 break 也并不能结束遍历。
为啥 for…of 内部就能让 await 生效呢。
因为 for…of 内部处理的机制和 forEach 不同,forEach 是直接调用回调函数,for…of 是通过迭代器的方式去遍历。
在 map 中使用 await
如果你在 map 中使用 await,map 将总是返回一个 promise 数组。
const mapLoop = async _ => {
console.log('Start')
const numFruits = await fruitsToGet.map(async fruit => {
const numFruit = await getNumFruit(fruit)
return numFruit
})
console.log(numFruits)
console.log('End')
}
'Start'
'[Promise, Promise, Promise]'
'End'
![Console loggs 'Start', '[Promise, Promise, Promise]', and 'End' immediately](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/30/17304d0ad5b02e60~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png)
如果你在 map 中使用 await,map 总是返回 promises,你必须等待 promises 数组得到处理。 或者通过 await Promise.all(arrayOfPromises) 来完成此操作。
const mapLoop = async _ => {
console.log('Start')
const promises = fruitsToGet.map(async fruit => {
const numFruit = await getNumFruit(fruit)
return numFruit
})
const numFruits = await Promise.all(promises);
console.log(numFruits);
console.log('End')
}
运行结果如下:
'Start'
'[27, 0, 14]'
'End'
![Console logs 'Start'. One second later, it logs '[27, 0, 14] and 'End'](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/30/17304d0c3329122c~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png)
如果你愿意,可以在promise 中处理返回值,解析后的将是返回的值。
const mapLoop = async _ => {
// ...
const promises = fruitsToGet.map(async fruit => {
const numFruit = await getNumFruit(fruit)
// Adds onn fruits before returning
return numFruit + 100
})
// ...
}
'Start'
'[127, 100, 114]'
'End'
在 filter 循环中使用 await
当你使用 filter 时,希望筛选具有特定结果的数组。假设过滤数量大于 20 的数组。
如果你正常使用 filter(没有 await),如下:
const filterLoop = _ => {
console.log('Start')
const moreThan20 = fruitsToGet.filter(fruit => {
const numFruit = fruitBasket[fruit]
return numFruit > 20
})
console.log(moreThan20)
console.log('End')
}
Start
["apple"]
END
filter 中的 await 不会以相同的方式工作,实际上,它根本不起作用,你会得到未过滤的数组。
const filterLoop = async _ => {
console.log('Start')
const moreThan20 = await fruitsToGet.filter(async fruit => {
const numFruit = await getNumFruit(fruit)
return numFruit > 20
})
console.log(moreThan20)
console.log('End')
}
'Start'
['apple', 'grape', 'pear']
'End'
这是为什么呢?
当你在 filter 回调中使用 await 时,回调总是会返回一个 promise。因为 promises 总是真的,数组中的所有项都通过filter 。在filter 使用 await类以下这段代码
const filtered = array.filter(() => true)
在filter使用 await 正确的三个步骤
- 使用map返回一个promise 数组
- 使用 await 等待处理结果
- 使用 filter 对返回的结果进行处理
const filterLoop = async _ => {
console.log('Start')
const promises = await fruitsToGet.map(fruit => getNumFruit(fruit))
const numFruits = await Promise.all(promises)
const moreThan20 = fruitsToGet.filter((fruit, index) => {
const numFruit = numFruits[index]
return numFruit > 20
})
console.log(moreThan20)
console.log('End')
}
Start
[ 'apple' ]
End
![Console shows 'Start'. One second later, console logs '['apple']' and 'End'](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/30/17304d0ae187d353~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png)
在 reduce 使用 await
如果想要计算 fruitBastet 中的水果总数。 通常可以使用 reduce 循环遍历数组并将数字相加。
const reduceLoop = _ => {
console.log('Start');
const sum = fruitsToGet.reduce((sum, fruit) => {
const numFruit = fruitBasket[fruit];
return sum + numFruit;
}, 0)
console.log(sum)
console.log('End')
}

当你在 reduce 中使用await时,结果会变得非常混乱。
const reduceLoop = async _ => {
console.log('Start')
const sum = await fruitsToGet.reduce(async (sum, fruit) => {
const numFruit = await getNumFruit(fruit)
return sum + numFruit
}, 0)
console.log(sum)
console.log('End')
}
'Start'
'[object Promise]14'
'End'
![Console logs 'Start'. One second later, it logs '[object Promise]14' and 'End'](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/30/17304d0ae630a22c~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png)
[object Promise]14 是什么 鬼??
剖析这一点很有趣。
- 在第一次遍历中,sum为0。numFruit是27(通过getNumFruit(apple)的得到的值),0 + 27 = 27。
- 在第二次遍历中,sum是一个promise。 (为什么?因为异步函数总是返回promises!)numFruit是0.promise 无法正常添加到对象,因此JavaScript将其转换为[object Promise]字符串。 [object Promise] + 0 是object Promise] 0。
- 在第三次遍历中,sum 也是一个promise。 numFruit是14. [object Promise] + 14是[object Promise] 14。
这意味着,你可以在reduce回调中使用await,但是你必须记住先等待累加器!
const reduceLoop = async _ => {
console.log('Start');
const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => {
const sum = await promisedSum;
const numFruit = await fruitBasket[fruit];
return sum + numFruit;
}, 0)
console.log(sum)
console.log('End')
}

但是从上图中看到的那样,await 操作都需要很长时间。 发生这种情况是因为reduceLoop需要等待每次遍历完成promisedSum。
有一种方法可以加速reduce循环,如果你在等待promisedSum之前先等待getNumFruits(),那么reduceLoop只需要一秒钟即可完成:
const reduceLoop = async _ => {
console.log('Start');
const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => {
const numFruit = await fruitBasket[fruit];
const sum = await promisedSum;
return sum + numFruit;
}, 0)
console.log(sum)
console.log('End')
}

这是因为reduce可以在等待循环的下一个迭代之前触发所有三个getNumFruit promise。然而,这个方法有点令人困惑,因为你必须注意等待的顺序。
在reduce中使用wait最简单(也是最有效)的方法是
- 使用map返回一个promise 数组
- 使用 await 等待处理结果
- 使用 reduce 对返回的结果进行处理
const reduceLoop = async _ => {
console.log('Start')
const promises = fruitsToGet.map(getNumFruit)
const numFruits = await Promise.all(promises)
const sum = numFruits.reduce((sum, fruit) => sum + fruit)
console.log(sum)
console.log('End')
}
这个版本易于阅读和理解,需要一秒钟来计算水果总数。

从上面看出来什么
- 如果你想连续执行await调用,请使用没有回调的循环(for…of 、 for 循环、 while循环)
- 永远不要和 forEach 一起使用await
- 不要在 filter 和 reduce 中使用 await,如果需要,先用 map 进一步骤处理,然后在使用 filter 和 reduce 进行处理。