如何等待递归异步函数的所有调用

How to wait for all calls of a recursive asynchronous function

我有这样一种情况,我必须从多个 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 并添加其子项,但随后 returns 仅深入一层,没有等待下一层调用继续进行。 fetchGroupData 函数不断被调用正确的次数,但出于某种原因,我只能从第一轮调用中访问数组。我怎样才能等到所有电话都结束?我试过在任何地方添加一堆等待,甚至在没有运气的情况下为 forEach 使用 promise.all。此外,如果有一种完全不同的方法来解决这个格式问题会更容易,那也将不胜感激。谢谢

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

因此,要修复它,您必须将所有新承诺链接到现有承诺链中,以便它们全部连接。有多种方法可以做到这一点。由于您似乎在传递一个每个人都在研究的通用数据结构 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 这意味着它将 return 一个承诺,该承诺将凭借 [=63= 挂钩到这个承诺链中] 从 .then() 处理程序中获取它。
  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() returns 的承诺在对它的所有递归调用都已解决之前不会解决。而且,它确保那些调用它的人也在关注它 return 的承诺,并且在该承诺解决之前不会前进。这将所有东西都连接到一个巨大的承诺链(和 await 链)中,因此事情被正确排序,你可以在一切完成后调用 functionToCallAfterAllRequestsAreMade(groups);

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

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)