一句话:
for/while/递归里的 await 会串行等待;forEach/map里的 await 不会阻塞循环——选错结构是异步顺序 bug 的头号原因。
写在前面
在循环里发请求、读文件、调接口时,「一个一个等」和「一起发」结果完全不同。 await 是否串行,取决于外层是不是真正的 async 函数作用域,而不是你写了 async 回调就行。本篇用同一组测试数据对比 for、while、递归、forEach、map 与 for await...of 的行为。
核心内容
测试辅助代码
javascript
const timeOut = (item, time = 1000) =>
new Promise(resolve => setTimeout(() => resolve(item), time))
const dataArr = [1, 2, 3, 4, 5]
串行执行(await 生效)
以下写法都会 按顺序 等待上一个 Promise 完成,约每 1 秒打印一个,总耗时 ~5s:
for
javascript
const forTest = async () => {
for (let i = 0; i < dataArr.length; i++) {
const res = await timeOut(dataArr[i])
console.log(res)
}
}
for...of
javascript
const forofTest = async () => {
for (const item of dataArr) {
const res = await timeOut(item)
console.log(res)
}
}
for...in(遍历数组得到索引字符串,需 dataArr[item])
javascript
const forinTest = async () => {
for (const key in dataArr) {
const res = await timeOut(dataArr[key])
console.log(res)
}
}
while
javascript
const whileTest = async () => {
let i = 0
while (i < dataArr.length) {
const res = await timeOut(dataArr[i++])
console.log(res)
}
}
递归
javascript
const diguiFunc = async (index = 0) => {
if (index < dataArr.length) {
const res = await timeOut(dataArr[index])
console.log(res)
await diguiFunc(index + 1) // 建议 await,便于错误传播
}
}
for await...of(消费异步可迭代对象)
javascript
const asyncIterable = [
timeOut(2, 2000),
timeOut(3, 3000),
timeOut(1, 1000)
]
const forAwaitOfTest = async () => {
for await (const item of asyncIterable) {
console.log(item) // 按迭代顺序:2 → 3 → 1(各等各自 delay)
}
}
并行执行(await 不阻塞外层)
forEach:回调里的 async 函数各自独立,外层不 await 它们。
javascript
const foreachTest = async () => {
dataArr.forEach(async (item) => {
const res = await timeOut(item)
console.log(res)
})
}
// 约 1s 后几乎同时打印 1 2 3 4 5(顺序不保证)
map + Promise.all(显式并发,全部完成再继续)
javascript
const mapTest = async () => {
await Promise.all(
dataArr.map(async (item) => {
const res = await timeOut(item)
console.log(res)
return res
})
)
}
// 同样约 1s 完成,但 mapTest 会等到全部结束
行为对比表
| 写法 | await 是否串行 | 适用 |
|---|---|---|
for / for...of / while |
是 | 依赖上一次结果的链式请求 |
| 递归 + await | 是 | 树形结构 DFS、分页直到空 |
for await...of |
是(按异步迭代器) | 异步生成器、ReadableStream |
forEach + async 回调 |
否 | 不推荐用于顺序异步 |
map + Promise.all |
并发 | 互不依赖的批量请求 |
应用场景
- 多图表依次渲染:用
for...of+ await,避免同时打满接口限流。 - 互不依赖的批量上传:
Promise.all(items.map(...))或限制并发的 pool(如 p-limit)。 - 分页拉全量:
while (hasNext) { await fetchPage(); page++ }或递归。
限制并发示例:
javascript
async function mapLimit(list, limit, fn) {
const ret = []
let i = 0
async function worker() {
while (i < list.length) {
const idx = i++
ret[idx] = await fn(list[idx], idx)
}
}
await Promise.all(Array.from({ length: limit }, worker))
return ret
}
踩坑
forEach里写 async 无法被外层 await:await arr.forEach(async () => {})无效。- 递归不 await 下一层:错误无法冒泡到顶层 try/catch。
for...in遍历数组:键是字符串且含原型链属性,遍历数组优先for...of。- Promise.all 一个失败全失败:需容错时用
Promise.allSettled。 - 串行太慢:100 个独立 GET 用 for+await 要 100×RTT;改并发 + 限流。
小结
- 要 顺序:
for/for...of/while/ 带 await 的递归。 - 要 并发:
Promise.all+map,必要时加并发上限。 - 不要用 forEach 做顺序异步;
for await...of专吃异步迭代源。


全部评论(0)