0

0

正确使用Promise.all()解决React中异步数据收集问题

心靈之曲

心靈之曲

发布时间:2025-11-01 13:15:22

|

534人浏览过

|

来源于php中文网

原创

正确使用Promise.all()解决React中异步数据收集问题

本文深入探讨了在react应用中从firestore等服务异步获取嵌套数据时遇到的常见问题:当尝试使用`foreach`循环收集异步操作的结果时,可能会因为异步回调未被等待而导致返回空数组或未定义的数据。文章详细阐述了如何通过结合使用`array.prototype.map()`生成promise数组,并利用`promise.all()`并发等待所有异步操作完成,从而确保数据正确收集并解决`undefined`访问问题,提供了专业的解决方案和最佳实践。

异步数据收集的挑战与常见陷阱

在现代Web应用开发中,尤其是在使用React和Firestore等NoSQL数据库时,从嵌套集合中异步获取数据是一个常见的需求。然而,不恰当地处理异步操作的集合可能会导致数据访问问题,例如尝试访问数组元素时得到undefined。这通常发生在开发者尝试在forEach循环中执行异步操作并收集结果时。

考虑以下一个从Firestore获取文档及其子集合“objectives”的函数GetObjectives:

import { collection, getDocs } from 'firebase/firestore';
import { db } from './firebaseConfig'; // 假设你的db实例已配置

export async function GetObjectives(docId) {
  try {
    const querySnapshot = await getDocs(collection(db, "users", "userIdNotAvailableForTheMoment", "documents", docId, "nodes"));
    const objectivesArray = [];

    // 问题所在:forEach不会等待内部的异步操作
    querySnapshot.forEach(async (nodeDoc) => {
      const nodeId = nodeDoc.id;
      const objectivesQuerySnapshot = await getDocs(collection(db, "users", "userIdNotAvailableForTheMoment", "documents", docId, "nodes", nodeId, "objectives"));

      const objectivesData = objectivesQuerySnapshot.docs.map((objectiveDoc) => ({
        ...objectiveDoc.data(),
        id: objectiveDoc.id
      }));
      objectivesArray.push(objectivesData);
    });

    // objectivesArray 在此处很可能为空,因为forEach内部的异步操作尚未完成
    return objectivesArray; 
  } catch (error) {
    console.error('Error during the recuperation of objectives : ', error);
    return null;
  }
}

当使用上述函数并尝试访问返回结果的第一个元素时,例如result[0],可能会得到undefined,即使console.log(result)可能看起来像一个包含数据的数组。这是因为Array.prototype.forEach方法本身是同步的,它不会等待其回调函数内部的异步操作完成。因此,当forEach循环结束后,objectivesArray很可能仍然是空的,或者只包含了部分未完全解析的数据,导致在函数返回时,外部调用者接收到一个空或不完整的数组。

解决方案:结合Promise.all()和Array.prototype.map()

要正确地收集多个异步操作的结果,我们需要确保所有内部的异步操作都已完成。JavaScript提供了Promise.all()方法来解决这个问题,它接收一个Promise数组,并返回一个新的Promise,该Promise在所有输入的Promise都解析后解析,并返回一个包含所有解析值的数组。

为了生成一个Promise数组供Promise.all()使用,我们可以利用Array.prototype.map()方法。map方法会为数组的每个元素调用一次回调函数,并使用回调函数的返回值创建一个新数组。如果回调函数是async函数,它将返回一个Promise,因此map会生成一个Promise数组。

以下是GetObjectives函数的修正版本:

import { collection, getDocs } from 'firebase/firestore';
import { db } from './firebaseConfig'; // 假设你的db实例已配置

export async function GetObjectives(docId) {
  try {
    const querySnapshot = await getDocs(collection(db, "users", "userIdNotAvailableForTheMoment", "documents", docId, "nodes"));

    // 使用map生成一个Promise数组
    const objectivesPromises = querySnapshot.docs.map(async (nodeDoc) => { // 注意这里是querySnapshot.docs
      const nodeId = nodeDoc.id;
      const objectivesQuerySnapshot = await getDocs(collection(db, "users", "userIdNotAvailableForTheMoment", "documents", docId, "nodes", nodeId, "objectives"));

      // 返回一个包含所有子目标数据的数组
      return objectivesQuerySnapshot.docs.map((objectiveDoc) => ({
        ...objectiveDoc.data(),
        id: objectiveDoc.id
      }));
    });

    // 使用Promise.all等待所有Promise解析,并返回一个包含所有结果的数组
    return Promise.all(objectivesPromises);
  } catch (error) {
    console.error('Error during the recuperation of objectives : ', error);
    return null;
  }
}

修正后的代码解析

  1. querySnapshot.docs.map(async (nodeDoc) => { ... }):

    • 我们现在对querySnapshot.docs(包含所有节点文档的数组)使用map方法,而不是forEach。
    • map的回调函数被标记为async,这意味着它总是返回一个Promise。
    • 在每个回调中,我们执行内部的异步操作await getDocs(...)来获取子集合“objectives”。
    • 回调函数最后返回一个包含该节点所有“objectives”数据的数组。由于回调是async的,这个返回的数组实际上被包装成了一个Promise(即Promise>)。
    • map方法最终会生成一个由这些Promises组成的数组,例如 [Promise, Promise, ...]。
  2. return Promise.all(objectivesPromises):

    • Promise.all()接收objectivesPromises数组。
    • 它会等待数组中的所有Promises都成功解析。
    • 一旦所有Promises都解析,Promise.all()自身会解析,并返回一个新数组,其中包含了所有内部Promises解析后的值(即每个节点的“objectives”数组)。
    • 这样,GetObjectives函数最终返回的将是一个完全解析的、包含所有目标数据的数组。

为什么map优于forEach进行异步收集

  • forEach: 主要用于遍历数组并执行副作用操作(如打印、修改外部变量)。它不返回任何值,并且不会等待其回调函数内部的异步操作。
  • map: 用于将数组的每个元素转换成新值,并返回一个包含这些新值的新数组。当回调函数返回Promise时,map会自然地生成一个Promise数组,这正是Promise.all()所需要的。

如何使用修正后的函数

调用修正后的GetObjectives函数将更加直观和可靠:

// 假设 sortDocuments[parseInt(selectedDocument)].id 提供了正确的docId
GetObjectives(sortDocuments[parseInt(selectedDocument)].id)
  .then((result) => {
    console.log(result);      // 应该会输出一个包含所有目标数据的嵌套数组
    console.log(result[0]);   // 现在应该能正确访问第一个节点的 objectives 数组
  })
  .catch((error) => {
    console.error("Failed to retrieve objectives:", error);
  });

在这种情况下,result将是一个类似以下结构的数组:

[
    [        
        {
            "parents": [],
            "childrens": [],
            "content": "value",
            "id": "VULpo5mPCc2kckLC9BCy",
            "title": "value"
        }
    ],
    [
        {
            "parents": [],
            "childrens": [],
            "content": "value",
            "id": "ANOTHER_ID",
            "title": "another value"
        }
    ]
]

现在,result[0]将正确地访问到第一个节点的目标数组。

注意事项与总结

  • 理解异步流: 深入理解async/await和Promise的工作原理是编写健壮异步代码的关键。async函数总是返回一个Promise,await关键字只能在async函数内部使用,它会暂停async函数的执行直到Promise解析。
  • 选择正确的迭代器: 当需要收集异步操作的结果时,map结合Promise.all()是比forEach更合适的模式。forEach适用于执行副作用,而不是收集返回值。
  • 错误处理: 始终在异步操作中使用try...catch块来捕获潜在的错误,提高应用的健壮性。Promise.all()会在其中任何一个Promise被拒绝时立即拒绝。
  • 并发限制: 对于大量并发的异步请求(例如,在一个非常大的数组上使用Promise.all()),可能会遇到性能瓶颈或API速率限制。在这种情况下,可能需要考虑使用Promise.allSettled()(获取所有Promise的结果,无论成功或失败)或实现自定义的并发限制器。

通过采纳Promise.all()与map的模式,开发者可以有效地管理复杂的异步数据流,确保数据被正确、完整地收集,从而避免因异步操作未完成而导致的undefined或空数据问题。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

557

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

395

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

756

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

478

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

474

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

1051

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

658

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

554

2023.09.20

菜鸟裹裹入口以及教程汇总
菜鸟裹裹入口以及教程汇总

本专题整合了菜鸟裹裹入口地址及教程分享,阅读专题下面的文章了解更多详细内容。

0

2026.01.22

热门下载

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

精品课程

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

共58课时 | 4万人学习

国外Web开发全栈课程全集
国外Web开发全栈课程全集

共12课时 | 1.0万人学习

React核心原理新老生命周期精讲
React核心原理新老生命周期精讲

共12课时 | 1万人学习

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

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