0

0

JavaScript中async/await与Fetch循环异步操作的最佳实践

碧海醫心

碧海醫心

发布时间:2025-10-31 11:05:58

|

339人浏览过

|

来源于php中文网

原创

JavaScript中async/await与Fetch循环异步操作的最佳实践

本文深入探讨了在javascript中使用`async/await`处理循环中的`fetch`请求时常见的陷阱。针对`foreach`无法正确等待异步操作的问题,我们提出并详细演示了如何结合`promise.all`与`array.prototype.map`,以高效、并行且结构清晰的方式管理多个异步网络请求,从而避免`await`语法错误并优化代码执行。

理解异步循环的挑战

在JavaScript中处理异步操作,特别是涉及到网络请求(如fetch API)时,async/await语法极大地简化了代码的编写和理解。然而,当我们需要在一个循环中执行一系列异步操作并等待它们全部完成时,常见的循环结构如forEach可能会导致意想不到的问题。

forEach方法是同步的。它会遍历数组中的每个元素,并为每个元素执行提供的回调函数,但它不会等待回调函数内部的异步操作完成。这意味着,即使你在forEach的回调函数中使用了async关键字和await表达式,forEach本身也不会暂停执行,而是会立即进入下一个迭代,导致外部代码在所有异步操作完成之前就继续执行。更严重的是,如果在非async函数或非async生成器、非模块的顶层作用域中直接使用await,JavaScript会抛出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.owner.username}`;
        allUserData.push(str);
    });

    // 问题:imprimirSchedule 很可能在 allUserData 填充完成之前就被调用
    imprimirSchedule(allUserData); // 假设 imprimirSchedule 只是打印数据
}

// 调用并等待所有调度信息获取完成(但实际上 forEach 内部的异步操作可能还没完成)
listarSchedules().then(() => console.log("DONE"));

尽管listarSchedules函数被声明为async,并且forEach的回调函数也被声明为async,但forEach本身并不会等待这些异步回调完成。这意味着imprimirSchedule(allUserData)可能会在一个空的或不完整的allUserData数组上执行。为了正确地等待所有异步请求完成,我们需要一种机制来收集所有Promise并等待它们全部解决。

立即学习Java免费学习笔记(深入)”;

解决方案:结合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));

实现细节与代码解析

  1. projetosList.map(async item => { ... }):

    • map方法遍历projetosList数组中的每个item。
    • async item => { ... } 为map的回调函数,它被声明为async,这意味着它内部可以使用await,并且每次执行都会返回一个Promise。
    • 因此,projetosList.map(...)的执行结果是一个由Promise组成的数组,每个Promise代表一个对getData的异步调用。
  2. const [projetoId, projetoNome] = item.split("#");:

    • 使用数组解构赋值从item字符串中提取projetoId和projetoNome,代码更简洁易读。
  3. const {description, owner:{username}} = await getData(urlSchedule);:

    • await getData(urlSchedule)会暂停当前async回调的执行,直到getData返回的Promise解决。
    • 一旦Promise解决,其结果(即API响应数据)会被赋值给{description, owner:{username}},利用对象解构赋值直接提取所需的属性。
  4. return${projetoId}#${description}#${username};:

    • 每个map回调函数返回一个格式化的字符串。这个字符串是该次getData调用成功后的最终结果。
    • 当Promise.all解决时,它将返回一个数组,其中包含了所有这些返回的字符串。
  5. 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的结果。

总结

在JavaScript中使用async/await处理循环中的异步fetch请求时,Promise.all与Array.prototype.map的组合是实现并行处理和正确等待所有异步操作完成的强大且优雅的模式。它不仅解决了forEach无法等待异步操作的问题,还提升了代码的可读性和执行

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
php中foreach用法
php中foreach用法

本专题整合了php中foreach用法的相关介绍,阅读专题下面的文章了解更多详细教程。

74

2025.12.04

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

530

2023.09.20

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

298

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

212

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1500

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

623

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

613

2024.03.22

php中定义字符串的方式
php中定义字符串的方式

php中定义字符串的方式:单引号;双引号;heredoc语法等等。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

588

2024.04.29

Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

10

2026.01.27

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 4.2万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 2.5万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号