先记住这个答案
forEach 内部就是一个普通的同步循环,每一项调用一次回调,回调返回什么它都不管。async 回调一调用就返回一个 Promise,forEach 把它丢掉,马上调用下一项。所以所有任务几乎是同时启动的,谁先完成取决于谁快,而 forEach 之后的代码也不会等它们。要按顺序执行,改成 for...of 加 await;要并发执行再统一等结果,用 await Promise.all(arr.map(async ...))。
forEach忽略回调返回值,async 回调的 Promise 没人等await只暂停回调自己,不暂停forEach- 按顺序执行用
for...of加await - 并发执行用
Promise.all(arr.map(async ...))
先看代码和输出
三个任务分别耗时 300、100、200 毫秒,期望按顺序完成,最后打印「全部结束」。
forEach 里 await 的实际效果JavaScriptconst sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const tasks = [300, 100, 200]
async function run() {
tasks.forEach(async (ms) => {
await sleep(ms) // 只暂停这个回调自己
console.log('完成', ms)
})
console.log('forEach 结束') // 不会等上面的任务
}
run()查看输出与解释
forEach 结束
完成 100
完成 200
完成 300forEach 结束 最先打印,说明 forEach 根本没等。后面三行按耗时从短到长出现,说明三个任务是同时开始的,和数组顺序无关。
为什么 await 挡不住 forEach
await 的作用范围只有它所在的那个 async 函数。在 forEach 里,这个函数是你传进去的回调,不是外面的 run。回调遇到 await 就暂停并返回一个 Promise,控制权回到 forEach。
forEach 的实现大致是 for (let i = 0; i < arr.length; i++) callback(arr[i], i, arr),它没有 await callback(...),也不收集返回值,最后固定返回 undefined。所以外面写 await tasks.forEach(...) 也没用,等的是 undefined,等于没等。
map 和 filter 这类数组方法同样不认识 Promise。区别在于 map 至少把 Promise 收集成了数组,你还能交给 Promise.all 去等;filter 拿 Promise 当判断条件,Promise 对象永远是真值,结果就是一项都不会被过滤掉。
两种正确写法
先想清楚要的是哪种:任务之间有依赖、或者必须控制顺序(比如按顺序写入、分页接着拉),用串行;任务互相独立,只想最快拿到全部结果,用并发。
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const tasks = [300, 100, 200]
async function serial() {
for (const ms of tasks) {
await sleep(ms) // 等这一个完成才进入下一轮
console.log('串行完成', ms)
}
}
async function parallel() {
const results = await Promise.all(tasks.map(async (ms) => {
await sleep(ms)
return ms
}))
console.log('并发结果', results.join(',')) // 按原数组顺序
}
serial().then(parallel)查看输出与解释
串行完成 300
串行完成 100
串行完成 200
并发结果 300,100,200串行版本总耗时是 300 + 100 + 200 = 600 毫秒,严格按数组顺序完成。并发版本总耗时约等于最慢的那个 300 毫秒,虽然完成顺序是 100、200、300,但 Promise.all 返回的结果数组始终和原数组一一对应。
- 需要限制并发数时
- 任务很多时,一次性
Promise.all会同时打出几百个请求,可能被浏览器排队或被服务端限流。这时要么分批,每批用Promise.all,要么写一个并发池,同一时刻最多跑 N 个,跑完一个再补一个。
容易答错的地方
- 以为
forEach里的错误能被外面的try/catch接住 - 每个 async 回调抛的错都进了它自己返回的 Promise,而这个 Promise 被
forEach扔掉了。外层try/catch什么都接不到,控制台只会出现未处理的拒绝。换成for...of或者await Promise.all,错误就能正常被try/catch捕获。 - 以为
Promise.all结果的顺序是完成顺序 - 结果顺序和传入数组的顺序一致,和谁先完成无关。这也是它比「在回调里
push结果」更可靠的原因,push的顺序才是完成顺序。 - 以为
for...of一定比forEach好 for...of加await是串行,互相独立的请求用它会白白多等。十个 200 毫秒的请求,串行要 2 秒,并发只要 200 毫秒左右。选哪种要看任务之间有没有依赖。
面试官还会怎么问?
for await...of 和 for...of 里写 await 有什么区别?
for await...of 是用来遍历异步可迭代对象的,比如流式读取的数据,每次取下一项本身就要等。遍历一个普通的 Promise 数组时,两者效果接近,但 for await...of 并不会延迟启动这些 Promise,它们在放进数组时就已经开始了。
不用 for...of,还能怎么让它串行?
用 reduce 串一条 Promise 链:tasks.reduce((prev, ms) => prev.then(() => sleep(ms)), Promise.resolve())。每一项都挂在上一项的 then 后面,所以严格按顺序执行。这是 async/await 出现之前的常见写法。
Promise.all 里有一个失败了会怎样?
Promise.all 会立刻以第一个失败的原因拒绝,但其他任务并不会被取消,还会在后台跑完。如果希望拿到每一项各自的成功或失败,改用 Promise.allSettled。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。