Async/await在forEach中:如何让异步与循环和谐共处

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

为什么你的async/await在forEach中失效?一眼看穿问题所在

\n\n

你是否曾经写过类似这样的代码,希望它能按顺序执行异步操作?

\n\n
const users = ['alice', 'bob', 'charlie'];\n\nusers.forEach(async (user) => {\n  const data = await fetch(`https://api.example.com/users/${user}`);\n  console.log(await data.json());\n});\n\nconsole.log('完成!');
\n\n

然后发现"完成!"在数据加载之前就输出了?很熟悉吧?你并不孤单。这是Stack Overflow上最热门的问题之一,拥有数千次浏览量。问题在于forEach是一个同步方法。它只是启动回调函数,但不会等待它们完成。你的async函数开始执行了,但forEach已经继续向前运行,就像一个不在乎其他人终点的短跑运动员。

\n\n

在本文中,我们不仅会展示如何修复这个问题。我们还会深入分析为什么会这样,并为你提供三个可行的解决方案及示例。你将了解何时使用Promise.all,何时使用经典的for...of。让我们开始吧!

\n\n

问题剖析:forEach与Promise

\n\n

让我们深入了解一下。JavaScript中的forEach方法是这样实现的:

\n\n
Array.prototype.forEach = function(callback) {\n  for (let i = 0; i < this.length; i++) {\n    callback(this[i], i, this);\n  }\n};
\n\n

看到了吗?它只是为每个元素调用callback,但不返回任何内容。即使回调函数返回一个Promise,forEach也会忽略它。它不会等待,不会收集结果——它只是启动然后遗忘。

\n\n

当你编写async (user) => { ... }时,你的函数返回一个Promise。但forEach不知道如何处理它。最终,所有请求都会并行启动(如果是fetch请求),但你无法等待它们完成。forEach之后的代码会立即执行。

\n\n

实际示例:在实践中是什么样子

\n\n

想象一下,你正在处理一个商品ID数组,需要从数据库中获取每个商品的价格:

\n\n
const productIds = [101, 102, 103];\n\nasync function getPrice(id) {\n  // 模拟数据库请求\n  return new Promise(resolve => {\n    setTimeout(() => {\n      resolve(`商品 ${id} 的价格: $${Math.floor(Math.random() * 100)}`);\n    }, 1000);\n  });\n}\n\nproductIds.forEach(async (id) => {\n  const price = await getPrice(id);\n  console.log(price);\n});\n\nconsole.log('所有价格已加载!'); // 立即执行,而不是3秒后
\n\n

控制台输出结果:

\n
所有价格已加载!\n商品 101 的价格: $42\n商品 102 的价格: $78\n商品 103 的价格: $15
\n\n

你同意吧,这不是我们期望的结果。好消息是:有一些简单而优雅的解决方案。

\n\n

解决方案一:for...of — 顺序执行

\n\n

让async/await在循环中工作的最简单方法是使用for...of。与forEach不同,for...of原生支持async/await,因为你在代码块内部控制执行流程。

\n\n
const productIds = [101, 102, 103];\n\nasync function getPrice(id) {\n  return new Promise(resolve => {\n    setTimeout(() => {\n      resolve(`商品 ${id} 的价格: $${Math.floor(Math.random() * 100)}`);\n    }, 1000);\n  });\n}\n\nasync function loadAllPrices() {\n  for (const id of productIds) {\n    const price = await getPrice(id);\n    console.log(price);\n  }\n  console.log('所有价格已加载!');\n}\n\nloadAllPrices();
\n\n

现在结果将是:

\n
商品 101 的价格: $42\n(1秒后)\n商品 102 的价格: $78\n(1秒后)\n商品 103 的价格: $15\n(1秒后)\n所有价格已加载!
\n\n

优点:简单、可读性强、顺序可预测。

\n

缺点:速度慢——每个请求都要等待上一个完成

博客

书籍推荐