forEach 循环中的异步操作问题

在 JavaScript 的 forEach 循环中处理异步操作(如接口请求)时,可能会遇到一些问题,因为 forEach 本身不会等待回调函数中的异步操作完成,就会继续同步执行下一个迭代。

问题分析

1
2
3
4
5
const array = ['a1', 'a2', 'a3'];
array.forEach(async (item) => {
await fetchData(item); // 异步操作不会按预期等待
});
console.log('Done'); // 这会在所有异步操作完成之前就执行
  • 所有请求会并行触发,但 forEach 不会等待它们完成。
  • 若希望顺序执行请求(一个接一个),forEach 无法实现。
  • 若希望等待所有请求完成后再处理结果,forEach 无法直接实现。

解决方案

使用 for...of 循环(顺序执行)

for...of循环会等待异步操作完成,因为它是在循环体内部使用await,这样可以按顺序执行每个异步操作。

1
2
3
4
5
6
7
async function processArray() {
for (const item of array) {
await fetchData(item); // 顺序执行,等待上一个完成
};
console.log('Done'); // 在所有异步操作完成后执行
};
processArray();

使用 Promise.all(并行执行)

如果你可以同时启动所有异步操作,并且不需要顺序执行,但需要等待所有操作完成,可以使用Promise.all

注意:Promise.all会同时启动所有异步操作,然后等待它们全部完成,如果其中任何一个失败,整个Promise.all会失败(可用 Promise.allSettled 替代)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function processArray() {
const promises = array.map(item => fetchData(item)); // 创建 Promise 数组
const results = await Promise.all(promises); // 并行执行所有请求
console.log('Done'); // 在所有异步操作完成后执行
};
processArray();

// 捕获每个任务的错误状态
async function processItems(items) {
const results = await Promise.allSettled(
items.map(item => processItem(item))
);

const successes = results.filter(r => r.status === 'fulfilled');
const failures = results.filter(r => r.status === 'rejected');

console.log(`成功: ${successes.length}, 失败: ${failures.length}`);
};

使用reduce实现(顺序执行)

1
2
3
4
5
6
7
async function processArray(array) {
await array.reduce(async (previousPromise, item) => {
await previousPromise;
return asyncOperation(item);
}, Promise.resolve());
console.log('Done');
};

使用 async.js 库(复杂控制)

对于更复杂的流程(如限制并发数),可以使用第三方库如 async.js

1
2
3
4
5
6
7
8
const async = require('async');

// 限制最多同时 2 个请求
async.eachLimit(array, 2, async (item) => {
await fetchData(item);
}, (err) => {
if (err) console.error(err);
});

实际应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 并行下载多个文件
async function downloadFiles(urls) {
const downloadPromises = urls.map(async (url, index) => {
const response = await fetch(url);
const blob = await response.blob();
return { index, blob };
});

const results = await Promise.all(downloadPromises);
return results.sort((a, b) => a.index - b.index);
};

// 顺序处理 API 请求(避免速率限制)
async function processWithRateLimit(items, limit = 100) {
for (let i = 0; i < items.length; i += limit) {
const batch = items.slice(i, i + limit);
const batchPromises = batch.map(item => apiCall(item));
await Promise.all(batchPromises);

// 添加延迟以避免速率限制
if (i + limit < items.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
};
};
};

总结建议

  1. 需要顺序执行时:使用 for...of 循环
  2. 需要并行执行时:使用 map + Promise.all
  3. 需要限制并发数时:使用专门的库如 async.jsp-limit 或手动控制
  4. 需要处理失败时:使用 Promise.allSettled
  5. 避免使用forEach 配合异步函数