新闻中心
J*aScript中async/await与Fetch循环异步操作的最佳实践

本文深入探讨了在j*ascript中使用`async/await`处理循环中的`fetch`请求时常见的陷阱。针对`foreach`无法正确等待异步操作的问题,我们提出并详细演示了如何结合`promise.all`与`array.prototype.map`,以高效、并行且结构清晰的方式管理多个异步网络请求,从而避免`await`语法错误并优化代码执行。
理解异步循环的挑战
在J*aScript中处理异步操作,特别是涉及到网络请求(如fetch API)时,async/await语法极大地简化了代码的编写和理解。然而,当我们需要在一个循环中执行一系列异步操作并等待它们全部完成时,常见的循环结构如forEach可能会导致意想不到的问题。
forEach方法是同步的。它会遍历数组中的每个元素,并为每个元素执行提供的回调函数,但它不会等待回调函数内部的异步操作完成。这意味着,即使你在forEach的回调函数中使用了async关键字和await表达式,forEach本身也不会暂停执行,而是会立即进入下一个迭代,导致外部代码在所有异步操作完成之前就继续执行。更严重的是,如果在非async函数或非async生成器、非模块的顶层作用域中直接使用await,J*aScript会抛出SyntaxError: await is only valid in async functions, async generators and modules.。
考虑以下示例代码片段,它试图在一个projetosList数组上循环,为每个项目异步获取调度信息:
// 假设 getData 函数已定义,它返回一个 Promise
function getData(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then((resp) => resp.json())
.then((data) => resolve(data))
.catch((error) => reject(error));
});
}
// 假设 projetosList 已经填充了项目数据,例如:
// let projetosList = ["123#Project A", "456#Project B"];
async function listarSchedules() {
let allUserData = [];
projetosList.forEach(async (item) => { // 注意这里的 async 回调
let projetoId = item.split("#")[0];
let urlSchedule = `https://gitlab.com/api/v4/projects/${projetoId}/pipeline_schedules?private_token=glpat-uSjCXDMEZPh5x6fChMxs`;
// 这里虽然使用了 await,但 forEach 不会等待
const data = await getData(urlSchedule); // getData 返回 Promise
// 假设 data 结构为 { description: "...", owner: { username: "..." } }
let str = `${projetoId}#${data.description}#${data.own
er.username}`;
allUserData.push(str);
});
// 问题:imprimirSchedule 很可能在 allUserData 填充完成之前就被调用
imprimirSchedule(allUserData); // 假设 imprimirSchedule 只是打印数据
}
// 调用并等待所有调度信息获取完成(但实际上 forEach 内部的异步操作可能还没完成)
listarSchedules().then(() => console.log("DONE"));尽管listarSchedules函数被声明为async,并且forEach的回调函数也被声明为async,但forEach本身并不会等待这些异步回调完成。这意味着imprimirSchedule(allUserData)可能会在一个空的或不完整的allUserData数组上执行。为了正确地等待所有异步请求完成,我们需要一种机制来收集所有Promise并等待它们全部解决。
解决方案:结合Promise.all与Array.prototype.map
解决上述问题的最佳实践是利用Array.prototype.map来创建一个Promise数组,然后使用Promise.all来等待所有这些Promise的解决。这种方法不仅能够确保所有异步操作都已完成,而且还能实现请求的并行执行,从而提高效率。
Array.prototype.map方法会遍历数组的每个元素,并对每个元素执行一个回调函数,然后将回调函数的返回值组成一个新的数组。当这个回调函数是async函数时,它会返回一个Promise。因此,map操作的结果将是一个Promise数组。
Promise.all方法接收一个Promise的可迭代对象(例如一个Promise数组),并返回一个新的Promise。这个新的Promise会在所有输入的Promise都解决(resolve)之后解决,其解决值是一个包含所有输入Promise解决值的数组,顺序与输入Promise的顺序一致。如果任何一个输入的Promise被拒绝(reject),那么Promise.all返回的Promise也会立即被拒绝。
下面是使用Promise.all和map重构后的listarSchedules函数:
// 假设 getData 函数和 imprimirSchedule 函数已定义
// function getData(url) { /* ... */ }
// function imprimirSchedule(data) { console.log(data); }
async function listarSchedules() {
// 使用 Promise.all 结合 map 来并行处理所有请求
const allUserData = await Promise.all(
projetosList.map(async item => {
// 解构项目ID和名称,提高可读性
const [projetoId, projetoNome] = item.split("#");
console.log(`PROJETO ID: ${projetoId}, PROJETO NOME: ${projetoNome}`);
let urlSchedule = `https://gitlab.com/api/v4/projects/${projetoId}/pipeline_schedules?private_token=glpat-uSjCXDMEZPh5x6fChMxs`;
console.log("urlSchedule:", urlSchedule);
// 等待 getData(urlSchedule) Promise 解决,并解构所需数据
// 假设 getData(url) 返回的数据对象包含 description 和 owner 属性,
// 且 owner 属性是一个对象,包含 username 属性。
const {description, owner:{username}} = await getData(urlSchedule);
// 返回处理后的数据字符串,这将成为 Promise.all 结果数组中的一个元素
return `${projetoId}#${description}#${username}`;
})
);
// 此时 allUserData 已经包含了所有请求的结果
imprimirSchedule(allUserData);
}
// 调用并等待所有调度信息获取完成
listarSchedules().then(() => console.log("DONE")).catch(error => console.error("Error fetching schedules:", error));实现细节与代码解析
-
projetosList.map(async item => { ... }):
- map方法遍历projetosList数组中的每个item。
- async item => { ... } 为map的回调函数,它被声明为async,这意味着它内部可以使用await,并且每次执行都会返回一个Promise。
- 因此,projetosList.map(...)的执行结果是一个由Promise组成的数组,每个Promise代表一个对getData的异步调用。
-
const [projetoId, projetoNome] = item.split("#");:
- 使用数组解构赋值从item字符串中提取projetoId和projetoNome,代码更简洁易读。
-
const {description, owner:{username}} = await getData(urlSchedule);:
- await getData(urlSchedule)会暂停当前async回调的执行,直到getData返回的Promise解决。
- 一旦Promise解决,其结果(即API响应数据)会被赋值给{description, owner:{username}},利用对象解构赋值直接提取所需的属性。
-
return${projetoId}#${description}#${username};:
- 每个map回调函数返回一个格式化的字符串。这个字符串是该次getData调用成功后的最终结果。
- 当Promise.all解决时,它将返回一个数组,其中包含了所有这些返回的字符串。
-
await Promise.all(...):
- 最关键的一步。它等待map生成的所有Promise都成功解决。
- 一旦所有Promise都解决,Promise.all将解决为一个包含所有结果的数组,并将其赋值给allUserData。
- 此时,allUserData数组中已经包含了所有项目的调度信息,可以安全地传递给imprimirSchedule函数。
注意事项与最佳实践
-
错误处理: Promise.all的特性是“全有或全无”。如果它接收的任何一个Promise被拒绝,Promise.all本身会立即拒绝,并返回第一个被拒绝的Promise的错误。在实际应用中,你可能需要用try/catch块来捕获await Promise.all(...)可能抛出的错误,或者在map的回调函数内部处理单个请求的错误(例如,使用try/catch并返回一个默认值或错误标记)。
// 示例:在 Promise.all 外部捕获错误 try { const allUserData = await Promise.all(...); imprimirSchedule(allUserData); } catch (error) { console.error("处理调度信息时发生错误:", error); } // 示例:在 map 内部处理单个请求错误,让 Promise.all 始终成功 const allUserData = await Promise.all( projetosList.map(async item => { try { const [projetoId] = item.split("#"); const urlSchedule = `...`; // 构建你的 URL const {description, owner:{username}} = await getData(urlSchedule); return `${projetoId}#${description}#${username}`; } catch (error) { console.warn(`获取项目 ${item} 调度信息失败:`, error); return `${item}#ERROR#UNKNOWN`; // 返回一个错误标记或默认值 } }) ); 并发限制: 对于非常大量的网络请求,Promise.all会同时发起所有请求。这可能会导致服务器过载、API限流或客户端资源耗尽。在这种情况下,你可能需要实现并发控制,例如使用第三方库(如p-limit)或手动创建队列来限制同时进行的请求数量。
Promise.allSettled: 如果你希望即使某些Promise失败,也能获取所有Promise的最终状态(无论是fulfilled还是rejected),那么Promise.allSettled是一个更好的选择。它返回一个Promise,该Promise在所有给定的Promise都已解决或拒绝后解决,其解决值是一个对象数组,每个对象描述了相应Promise的结果。
总结
在J*aScript中使用async/await处理循环中的异步fetch请求时,Promise.all与Array.prototype.map的组合是实现并行处理和正确等待所有异步操作完成的强大且优雅的模式。它不仅解决了forEach无法等待异步操作的问题,还提升了代码的可读性和执行
以上就是J*aScript中async/await与Fetch循环异步操作的最佳实践的详细内容,更多请关注其它相关文章!
# 遍历
# 东方甄选营销推广方案
# 北京自制网站建设调整
# 杯子软文营销推广方案
# 河南抖音营销推广排名
# 做网站推广多少钱一次啊
# 破魔seo代运营
# 怎样做好营销推广策略
# 宁波关键词排名查看
# 数据种草营销推广报价
# 织梦网站wordpress优化
# 任何一个
# 所需
# 会在
# 重构
# javascript
# 迭代
# 组中
# 被拒
# 是一个
# 回调
# 可迭代对象
# 作用域
# gitlab
# ai
# 回调函数
# json
# git
# js
# java
相关栏目:
【
科技资讯46185 】
【
网络学院92790 】
相关推荐:
2026春节假期时间安排 2026春节假日查询
曝R星经典之作开发图 设计简陋但信息密集!
ACG动漫手机版官网入口 手机ACG动漫APP在线观看正版
Win11文件资源管理器卡顿怎么修 Win11重置资源管理器进程优化响应速度【修复方法】
c++ 获取系统当前时间 c++时间戳获取方法
处理Kafka消费者会话超时:深入理解消息处理语义与幂等性
深入理解与实现最大堆的Heapify过程:常见错误与修正
批改网学生版PC登录 批改网官网登录系统入口
React中useState与局部变量:理解组件状态管理与渲染机制
Lar*el 递归关系中排除指定分支的教程
虫虫漫画精品漫画官网_虫虫漫画精品漫画官网进入精品漫画
Win10系统服务哪些可以禁用 Win10安全优化服务列表【干货】
最新韩小圈网页版登录入口_官网在线观看官方链接
Mac终端命令大全_Mac常用Terminal指令速查
如何使用J*aScript精确选择并批量修改特定父元素下子链接的样式
Animex动漫社网入口地址 Animex动漫社网正版在线入口
蛙漫限时开放最深处链接_蛙漫全站漫画会员同款秒开地址
基于动态规划的房屋花卉种植最小成本算法详解
谷歌浏览器最新官方入口链接 谷歌浏览器网页版官网导航
C++ typeid如何获取类型信息_C++ RTTI运行时类型识别用法
LINUX下如何进行磁盘分区_fdisk与parted工具在LINUX中的使用对比
在Blazor WebAssembly应用中动态注入客户端特定指标代码的策略
MAC如何安全彻底地删除文件_MAC使用终端命令确保文件无法被恢复
c++如何使用Catch2编写单元测试_c++简洁易用的BDD风格测试框架
Python中高效访问嵌套字典与列表中的键值对
反效果?《战地6》免费试玩开启后玩家数不升反降
J*a里如何实现订单支付与库存同步功能_支付库存同步项目开发方法说明
学习通网页版官方登录 超星学习通电脑端入口指南
QQ邮箱官网登录入口 QQ邮箱网页版邮箱快速登录
抖音怎么赚钱_抖音创作者变现方法与途径指南
如何修改开机登录密码_Windows账户安全设置超详细教程【必学】
Win11怎么关闭快速启动_Win11彻底关机设置教程
在React函数组件中利用原生HTML5进行邮箱地址验证
快手极速版在线观看 官方网页版登录地址
天眼查怎么看公司融资情况 天眼查企业融资历史查询步骤【攻略】
2026年CSGO开箱网站推荐 CSGO开箱平台精选
Go语言中的*string:深入理解字符串指针
QQ邮箱官方登录入口_QQ邮箱网页版快捷使用平台
必由学网页版入口 必由学官方平台直接访问
C#使用XPath查询节点时出错? 常见语法错误与调试技巧
C++如何比较两个字符串_C++ string compare函数与操作符对比
Windows10怎么开启夜间模式 Windows10系统设置调整色温与亮度缓解夜间用眼疲劳【教程】
知乎APP怎么管理已购盐选内容_知乎APP盐选内容购买记录与查看方法
蛙漫2日版入口 WAMAN2(日版)无删减漫画官网链接
C++如何实现线程池_C++11手动实现一个简单的固定大小线程池
Pandas DataFrame:高效添加条件计算列
纯CSS与HTML网格布局的HTML精简策略:SVG与JS方案解析
蛙漫漫画免费阅读入口_蛙漫官方正版无广告纯净版
谷歌浏览器怎么给标签页静音_Chrome标签静音快捷操作
百度网盘网页版入口 百度网盘网页版官方登录网址


2025-10-31
浏览次数:次
返回列表
er.username}`;
allUserData.push(str);
});
// 问题:imprimirSchedule 很可能在 allUserData 填充完成之前就被调用
imprimirSchedule(allUserData); // 假设 imprimirSchedule 只是打印数据
}
// 调用并等待所有调度信息获取完成(但实际上 forEach 内部的异步操作可能还没完成)
listarSchedules().then(() => console.log("DONE"));