如何在异步函数中处理多个等待

Posted

技术标签:

【中文标题】如何在异步函数中处理多个等待【英文标题】:How to handle multiple awaits in async function 【发布时间】:2020-03-07 23:17:04 【问题描述】:

我有多个 API 调用要进行,它们通过 API 获取,通过 API 将数据写入 DB,通过另一个 API 将输出发送到前端。

我已经用下面的 await 编写了异步函数 -

前 2 个应该一个接一个地运行,但第三个可以独立运行,无需等待前 2 个 fetch 语句完成。

let getToken= await fetch(url_for_getToken);
let getTokenData = await getToken.json();

let writeToDB = await fetch(url_for_writeToDB);
let writeToDBData = await writeToDB.json();

let frontEnd = await fetch(url_for_frontEnd);
let frontEndData = await frontEnd.json();

处理这样多个 fetch 语句的最佳方法是什么?

【问题讨论】:

你可以看看Promise.all。 @YannickK Promise.all 在这里有必要吗?他不能只使用 .then() 代替吗?他不是在等待两个都完成,而是第一个然后第二个,然后第三个,不管这两个。 @Kobe 我认为这种情况下的主要问题是 OP 希望将服务器和客户端调用分开,因为它们彼此不依赖 - 它会是如果他们互相等待,这在性能上是愚蠢的 - 但如果 任何 他们失败了,你想要拒绝。你绝对正确,他可以不用Promise.all,但在这种情况下,我想如果他将所有内容都包含在一个Promise.all 调用中,它会更干净(并且将来更容易构建),特别是针对错误处理。 @Kobe 因为Promise.all is essential for proper error handling 并等待第一个然后第二个和第三个承诺的完成。 simplest answer 最好地解决了这个问题,但不幸的是,它不该被否决。值得一试,@Yasar Abdulllah。 【参考方案1】:

有很多方法,但最通用的是将每个异步代码路径包装在一个异步函数中。这使您可以根据需要灵活地混合和匹配异步返回值。在您的示例中,您甚至可以使用 async iife 内联代码:

await Promise.all([
  (async() => 
    let getToken = await fetch(url_for_getToken);
    let getTokenData = await getToken.json();

    let writeToDB = await fetch(url_for_writeToDB);
    let writeToDBData = await writeToDB.json();
  )(),
  (async() => 
    let frontEnd = await fetch(url_for_frontEnd);
    let frontEndData = await frontEnd.json();
  )()
]);

【讨论】:

终于有正确答案了! :D【参考方案2】:

如果您使用 Promise “创建者”(= 返回 Promise 的函数)而不是原始 Promise,会更容易。首先,定义:

const fetchJson = (url, opts) => () => fetch(url, opts).then(r => r.json())

返回这样一个“创造者”。现在,这里有两个用于串行和并行链接的实用程序,它们同时接受原始承诺和“创建者”:

const call = f => typeof f === 'function' ? f() : f;

const parallel = (...fns)  => Promise.all(fns.map(call));

async function series(...fns) 
    let res = [];

    for (let f of fns)
        res.push(await call(f));

    return res;

那么,主要代码可以这样写:

let [[getTokenData, writeToDBData], frontEndData] = await parallel(
    series(
        fetchJson(url_for_getToken),
        fetchJson(url_for_writeToDB),
    ),
    fetchJson(url_for_frontEnd),
)

如果您不喜欢专用的“创建者”包装器,您可以正常定义fetchJson

const fetchJson = (url, opts) => fetch(url, opts).then(r => r.json())

并在调用 seriesparallel 的地方使用内联延续:

let [[getTokenData, writeToDBData], frontEndData] = await parallel(
    series(
        () => fetchJson('getToken'),
        () => fetchJson('writeToDB'),
    ),
    () => fetchJson('frontEnd'), // continuation not necessary, but looks nicer
)

为了让这个想法更进一步,我们可以让seriesparallel 也返回“创造者”而不是承诺。这样,我们可以构建串行和并行承诺的任意嵌套“电路”并按顺序获得结果。完整的工作示例:

const call = f => typeof f === 'function' ? f() : f;

const parallel = (...fns)  => () => Promise.all(fns.map(call));

const series = (...fns) => async () => 
    let res = [];

    for (let f of fns)
        res.push(await call(f));

    return res;
;

//

const request = (x, time) => () => new Promise(resolve => 
    console.log('start', x);
    setTimeout(() => 
        console.log('end', x)
        resolve(x)
    , time)
);

async function main() 
    let chain = series(
        parallel(
            series(
                request('A1', 500),
                request('A2', 200),
            ),
            series(
                request('B1', 900),
                request('B2', 400),
                request('B3', 400),
            ),
        ),
        parallel(
            request('C1', 800),
            series(
                request('C2', 100),
                request('C3', 100),
            )
        ),
    );

    let results = await chain();

    console.log(JSON.stringify(results))


main()
.as-console-wrapper  max-height: 100% !important; top: 0; 

【讨论】:

你为什么不直接从fetchJson返回承诺?我看不到用另一个函数包装承诺的任何好处。 series 的有用性值得怀疑,因为系列中的下一个函数通常需要前一个函数的返回值。 啊,是的。所有这些仪式只是为了使用series。我更喜欢 async iife 序列和Promise.allSettled 而不是parallel @georg 你的解决方案看起来很复杂——你能解释一下你的方法有什么优势吗? @georg - 好的,但你能回答我的问题 - 你在这个答案中使用的通用技术的优势是什么?通常更通用的代码需要一些关于隐藏在背后的想法和它提供的优势/功能的文档(例如框架)。【参考方案3】:

您可以使用.then(),而不是等待:

fetch(url_for_getToken)
  .then(getToken => getToken.json())
  .then(async getTokenData => 
    let writeToDB = await fetch(url_for_writeToDB);
    let writeToDBData = await writeToDB.json();
    // Carry on
  )

fetch(url_for_frontEnd)
  .then(frontEnd => frontEnd.json())
  .then(frontEndData => 
    // Carry on  
  )

【讨论】:

不要忘记处理这些承诺链上的错误 - 或Promise.all 并将它们返回给调用者。【参考方案4】:

在开头运行独立请求 (writeToDB),不带await

let writeToDB = fetch(url_for_writeToDB);

let getToken = await fetch(url_for_getToken);
let getTokenData = await getToken.json();

// ...

【讨论】:

@Bergi 问题与错误处理无关——错误处理(如 try-catch)很重要,但它是单独的主题 await 内置了错误处理功能——当 promise 被拒绝时,你会得到一个异常,async function 调用返回的 promise 将被拒绝,调用者会注意到。如果您建议删除 await 关键字并独立运行 Promise 链,则必须在不考虑错误处理的情况下这样做。至少没有提及它(或保留呼叫者被拒绝的原始行为)的答案是错误的答案。 这个答案解决了 OP 的问题,也是迄今为止所有给出的答案中最简单的。我很好奇为什么这被否决了? OP 声明“第三个可以独立运行,无需等待前 2 个 fetch 语句完成。”,所以先执行它就可以了。 感谢@Bergi 的评论。鉴于 OP 的问题也不包括错误处理,我认为为了简洁和超出问题的范围而将其排除在外。但是,是的,任何人都可以根据自己的意见投票。 @AndreasPizsa 正如我所说,await 确实通过抛出异常来处理承诺错误,这可能会在某处被捕获(在未显示的代码中)。但是这个答案不提就改变了代码的错误处理行为,很危险。

以上是关于如何在异步函数中处理多个等待的主要内容,如果未能解决你的问题,请参考以下文章

如何在 AWS Lambda 中等待异步操作?

如何快速使用异步函数?完成处理程序[重复]

如何等待来自 forEach 循环的多个异步调用?

如何在c#中正确实现等待异步[重复]

如何使用回调在任何函数中调用异步等待方法

如何在异步等待函数中返回值?