【问题标题】:How to wait for all calls of a recursive asynchronous function如何等待递归异步函数的所有调用
【发布时间】:2022-01-27 12:15:16
【问题描述】:

我有一种情况,我必须多次从多个 api 获取数据,以便将所有数据转换为嵌套格式。基本上,我必须从一个 api 获取可用组的列表,然后单独获取每个组的数据,并为任何子组重复。我提出的解决方案使用递归从第一个请求中获取数据并将嵌套属性添加到原始数组:

fetch("/mygroups")
  .then((res) => res.json())
  .then((data) => {
    return data.map((group) => ({ value: group.id, label: group.name, children: [] }));
  })
  .then((groups) => {
    groups.forEach((group) => {
      fetchGroupData(group);
    });
    return groups;
  })
  .then((groups) => {
    // I need this groups variable to be the final groups variable with all it's
    // nested children
    functionToCallAfterAllRequestsAreMade(groups);
  });

async function fetchGroupData(group) {
  const res = await fetch(`/groups/${group.value}`);
  const data = await res.json();
  // A group can have locations and/or more groups as its children.
  // if it has groups, it will call fetchGroupData for those
  // child groups
  const locations = data.locations.map((location) => ({
    value: location.id,
    label: location.location_name,
  }));
  const groups = data.groups.map((group) => ({
    value: group.id,
    label: group.name,
    children: [],
  }));
  group.children = [...groups, ...locations];
  if (groups.length > 0) {
    group.children.forEach((child) => {
      if (child.hasOwnProperty("children")) {
        fetchGroupData(child);
      }
    });
  }
}

问题在于,此代码似乎为初始组数组调用 fetchGroupData 并添加其子级,但随后仅返回一层深,而无需等待下一层调用继续进行。 fetchGroupData 函数不断被调用正确的次数,但由于某种原因,我只能从第一轮调用中访问数组。我怎样才能等到所有电话都完成?我已经尝试在任何地方添加一堆等待,甚至使用 promise.all 作为 forEach 没有运气。此外,如果有一种完全不同的方法来解决这个格式问题,这会更容易,那也将不胜感激。谢谢。

【问题讨论】:

  • 你能发布一个 JSON 的例子吗?不是整个事情,就像几个分支一样。

标签: javascript asynchronous recursion async-await promise


【解决方案1】:

您当前代码的问题是您对fetchGroupData() 进行了多次调用,而您既没有链接到现有的promise,也没有链接到awaiting。因此,它们最终成为独立的承诺链,与您要检查最终数据的.then() 完全没有联系。

因此,要修复它,您必须将所有新的 Promise 链接到现有的 Promise 链中,以便它们全部连接。有多种方法可以做到这一点。由于您似乎在传递一种每个人都在处理groups 的通用数据结构,因此我选择了在所有其他异步调用中使用await 的方法来强制对所有内容进行排序。可能可以并行化两个循环并使用Promise.all(),但是您在每个子调用中修改单个groups 数据结构的方式意味着您不能保留任何顺序,因此不运行似乎更安全并行。

请注意,我还删除了 .forEach() 循环,因为它们不支持承诺,我将其更改为常规的 for 循环,该循环支持承诺,并将使用 await 暂停其迭代。

fetch("/mygroups")
    .then((res) => res.json())
    .then(async (data) => {
        const groups = data.map((group) => ({ value: group.id, label: group.name, children: [] }));
        for (let group of groups) {
            await fetchGroupData(group);
        }
        return groups;
    })
    .then((groups) => {
        // I need this groups variable to be the final groups variable with all it's
        // nested children
        functionToCallAfterAllRequestsAreMade(groups);
    });

async function fetchGroupData(group) {
    const res = await fetch(`/groups/${group.value}`);
    const data = await res.json();
    // A group can have locations and/or more groups as its children.
    // if it has groups, it will call fetchGroupData for those
    // child groups
    const locations = data.locations.map((location) => ({
        value: location.id,
        label: location.location_name,
    }));
    const groups = data.groups.map((group) => ({
        value: group.id,
        label: group.name,
        children: [],
    }));
    group.children = [...groups, ...locations];
    if (groups.length > 0) {
        for (let child of group.children) {
            if (child.hasOwnProperty("children")) {
                await fetchGroupData(child);
            }
        }
    }
}

变化:

  1. 合并来自两个.then() 处理程序的代码,因为其中一个包含纯同步代码。制作一个.then() 处理程序async,这样我们就可以在其中使用await,这意味着它将返回一个promise,该promise 将通过从.then() 处理程序返回它而挂钩到这个promise 链中。
  2. groups.forEach(...) 更改为for (let group of groups) { ... },以便我们可以使用await 并暂停循环。
  3. fetchGroupData(group); 更改为await fetchGroupData(group);
  4. fetchGroupData() 中再次将group.children.forEach((child) => { ...}) 更改为for (let child of group.children) { ... });,这样我们就可以使用await 来暂停循环。
  5. fetchGroupData() 中将fetchGroupData(group); 更改为await fetchGroupData(group);

这确保fetchGroupData() 返回的承诺在所有递归调用都解决之前不会解决。而且,它确保调用它的人也在观察它返回的承诺,并且在该承诺解决之前不要前进。这会将所有内容连接到一个巨大的 Promise 链(和 await 链)中,因此事情会正确排序,当一切完成后您可以调用 functionToCallAfterAllRequestsAreMade(groups);

【讨论】:

    【解决方案2】:

    我建议将您的功能分解为独立且易于管理的部分 -

    function getJson(url) {
      return fetch(url).then(r => r.json())
    }
    

    我们写fetchGroups(复数)来获取所有组-

    async function fetchGroups(url) {
      const groups = await getJson(url)
      return Promise.all(groups.map(fetchGroup))
    }
    

    fetchGroup(单数)获取单个组 -

    async function fetchGroup({ id, name, locations = [], groups = [] }) {
      return {
        value: id,
        label: name,
        children: [
          ...locations.map(l => ({ value: l.id, label: l.location.name })),
          ...await fetchGroups(`/groups/${id}`)
        ]
      }
    }
    

    这种函数排列称为相互递归,非常适合创建、遍历和操作递归结构,例如您问题中的树。

    现在我们只需在您的初始路径上调用fetchGroups -

    fetchGroups("/mygroups")
      .then(console.log, console.error)
    

    【讨论】:

      猜你喜欢
      • 2020-09-08
      • 2021-05-12
      • 2015-08-27
      • 2021-08-28
      • 1970-01-01
      • 2016-02-21
      • 2018-12-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多