技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

为啥 await 不能用在 forEach 中

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/77-为啥 await 不能用在 forEach 中
本文目录7 个章节
  1. 一、原文示例发生了什么
  2. 二、需要并行执行:map + Promise.all
  3. 三、需要串行执行:for...of
  4. 四、限制并发数
  5. 五、什么时候用哪一种
  6. 六、for await...of 的补充
  7. 总结

为啥 await 不能用在 forEach

Category(分类): JavaScript Status: 已整理

更准确的标题应该是:forEach 不会等待 async 回调返回的 Promiseawait 当然可以写在 forEachasync 回调内部,只是外层的 forEach 和包含它的函数不会自动等待这些 Promise。

一、原文示例发生了什么

function fetchValue(value) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), 500 * value)
  })
}

function test() {
  const values = [3, 2, 1]

  values.forEach(async (value) => {
    const result = await fetchValue(value)
    console.log(result)
  })

  console.log('end')
}

test()

输出通常是:

end
1
2
3

forEach 会同步地依次调用三个回调。每个 async 回调第一次执行到 await 时就返回一个 Promise,forEach 不会保存、更不会等待这个返回值,于是外层函数立即打印 end。三个定时器的延迟分别是 1500、1000、500 毫秒,所以结果按完成时间输出 1、2、3

原文把原因概括成“forEach 只支持同步代码”,这不够准确:回调中可以执行异步代码,问题只是 forEach 的返回值始终是 undefined,不会建立 Promise 链,也不会把回调的异常传给调用者。

原文配图:forEach 不等待异步回调

二、需要并行执行:map + Promise.all

如果任务之间互不依赖,希望同时开始并在全部完成后继续,应把每个异步回调的 Promise 收集起来:

function fetchValue(value) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), 500 * value)
  })
}

async function runInParallel() {
  const values = [3, 2, 1]
  const results = await Promise.all(
    values.map((value) => fetchValue(value))
  )

  console.log(results) // [3, 2, 1]:结果顺序与输入一致
  console.log('end')
}

runInParallel().catch(console.error)

这里三个任务几乎同时开始,因此约 500 毫秒后最先完成值为 1 的任务,但 Promise.all 的结果数组仍按输入顺序排列。若在任务内部 console.log,日志仍可能按完成顺序出现:

async function logInParallel() {
  const values = [3, 2, 1]

  await Promise.all(
    values.map(async (value) => {
      const result = await fetchValue(value)
      console.log(result) // 通常为 1、2、3
      return result
    })
  )

  console.log('end') // 所有任务完成后才执行
}

所以原文说“Promise.all 不行”需要改成:如果期望串行完成,Promise.all 不符合目标;如果期望并行开始并等待全部结束,Promise.all 正是合适的方案。

原文配图:Promise.all 并行任务

错误处理

async function runAll() {
  const values = [1, 2, 3]

  try {
    await Promise.all(
      values.map(async (value) => {
        if (value === 2) throw new Error('value 2 failed')
        return fetchValue(value)
      })
    )
  } catch (error) {
    console.error('至少一个任务失败:', error.message)
  }
}

Promise.all 在一个输入 Promise rejected 后会尽快 rejected,但其他已经启动的任务不会被自动取消。需要知道每一项结果而不是遇到第一项失败就结束时,可以使用 Promise.allSettled

const results = await Promise.allSettled(
  [1, 2, 3].map((value) => fetchValue(value))
)

for (const result of results) {
  if (result.status === 'fulfilled') {
    console.log('success:', result.value)
  } else {
    console.error('failed:', result.reason)
  }
}

三、需要串行执行:for...of

如果后一个任务依赖前一个任务的结果,或者服务端要求按顺序请求,可以使用 for...of

async function runSerially() {
  const values = [3, 2, 1]

  for (const value of values) {
    const result = await fetchValue(value)
    console.log(result) // 3、2、1
  }

  console.log('end')
}

runSerially().catch(console.error)

for...of 每次循环体都属于同一个 async 函数执行流程。当前一轮遇到 await 时,函数暂停;Promise fulfilled 后,函数才恢复并进入下一轮。它不是因为“使用了迭代器就自动支持 await”,真正起作用的是 await 所在的 async 函数以及循环体之间没有被丢弃的控制流。

原文配图:for...of 串行等待

普通 for 循环也可以实现同样的效果:

async function runWithFor() {
  const values = [3, 2, 1]

  for (let index = 0; index < values.length; index += 1) {
    const result = await fetchValue(values[index])
    console.log(result)
  }
}

如果循环中需要使用索引、breakcontinue 或修改循环变量,普通 for 可能更方便。for...offor 都支持 breakcontinue,而 forEach 的回调中不能用它们控制外层遍历。

原文配图:普通 for 串行等待

四、限制并发数

全部并行可能压垮浏览器连接、服务端或数据库;全部串行又可能太慢。可以启动固定数量的 worker,按输入顺序领取任务:

async function mapWithConcurrency(items, limit, worker) {
  if (!Number.isInteger(limit) || limit < 1) {
    throw new RangeError('limit must be a positive integer')
  }

  const results = new Array(items.length)
  let nextIndex = 0

  async function consume() {
    while (true) {
      const index = nextIndex
      nextIndex += 1
      if (index >= items.length) return

      results[index] = await worker(items[index], index)
    }
  }

  const workerCount = Math.min(limit, items.length)
  await Promise.all(
    Array.from({ length: workerCount }, () => consume())
  )
  return results
}

async function demoLimitedConcurrency() {
  const values = [1, 2, 3, 4, 5]
  const results = await mapWithConcurrency(values, 2, async (value) => {
    await fetchValue(value)
    return value * 2
  })

  console.log(results) // [2, 4, 6, 8, 10]
}

demoLimitedConcurrency().catch(console.error)

上例使用了顶层 await,需要放在 ES 模块中运行;在 CommonJS 中可以包进一个 async 函数。并发限制只控制同时运行的 worker 数量,不会自动取消已经开始的任务。

五、什么时候用哪一种

目标推荐写法特点
不关心异步结果,也不需要等待forEach 中调用明确的副作用函数必须自行处理错误和生命周期,不推荐丢弃 Promise
所有任务并行,等全部完成Promise.all(values.map(worker))快,但可能产生大量并发
所有任务完成,无论成功失败Promise.allSettled可逐项检查状态
按顺序执行for...of 或普通 for易读,可 break/continue
控制并发量worker pool 或并发限制库在吞吐量和资源占用之间折中

当任务产生了副作用(写入、删除、发送请求)时,不要为了“看起来简洁”把 Promise 丢给 forEach。明确选择并行、串行或限并发策略,并在最外层处理 rejected Promise。

六、for await...of 的补充

for await...of 用于消费异步可迭代对象,也可以消费普通可迭代对象中的 Promise。它适合流、分页器和异步生成器,不是普通数组异步遍历的唯一答案:

async function* valuesFromServer() {
  yield fetchValue(1)
  yield fetchValue(2)
  yield fetchValue(3)
}

for await (const value of valuesFromServer()) {
  console.log(value)
}

异步迭代器还可能实现 return(),因此在提前 break 时可以进行资源清理。处理文件流、网络流时,应同时了解具体 API 的取消和关闭机制。

总结

await 不是不能写在 forEach 中,而是 forEach 不会等待 async 回调返回的 Promise。选择循环方式时先明确需求:

  • 需要串行:for...offor
  • 需要并行并等待全部:Promise.all
  • 需要收集失败和成功:Promise.allSettled
  • 需要控制资源:限制并发数。

参考资料:

原文作者:iDo。参考:async,await 与 forEach 引发的血案

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS