【问题标题】:for await of VS Promise.all等待 VS Promise.all
【发布时间】:2020-04-28 20:23:04
【问题描述】:

这有什么区别:

const promises = await Promise.all(items.map(e => somethingAsync(e)));
for (const res of promises) {
  // do some calculations
}

还有这个?

for await (const res of items.map(e => somethingAsync(e))) {
  // do some calculations
}

我知道在第一个 sn-p 中,所有的 Promise 都会同时触发,但我不确定第二个。 for 循环是否等待第一次迭代完成以调用下一个 promise ?还是所有的 Promise 都是同时触发的,而循环内部对它们来说就像一个回调?

【问题讨论】:

    标签: javascript node.js promise async-await for-await


    【解决方案1】:

    正如您所说,Promise.all 将一次性发送所有请求,然后在所有请求完成后您将收到响应。

    在第二种情况下,您将一次性发送请求,但会逐一收到响应。

    请参阅这个小示例以供参考。

    let i = 1;
    function somethingAsync(time) {
      console.log("fired");
      return delay(time).then(() => Promise.resolve(i++));
    }
    const items = [1000, 2000, 3000, 4000];
    
    function delay(time) {
      return new Promise((resolve) => { 
          setTimeout(resolve, time)
      });
    }
    
    (async() => {
      console.time("first way");
      const promises = await Promise.all(items.map(e => somethingAsync(e)));
      for (const res of promises) {
        console.log(res);
      }
      console.timeEnd("first way");
    
      i=1; //reset counter
      console.time("second way");
      for await (const res of items.map(e => somethingAsync(e))) {
        // do some calculations
        console.log(res);
      }
      console.timeEnd("second way");
    })();
    
    

    你也可以在这里试试 - https://repl.it/repls/SuddenUselessAnalyst

    希望这会有所帮助。

    【讨论】:

    • 事实上你证明了我的第二点。使用for await 实际上会同时触发所有的promise,因此循环的执行时间:4 秒。如果它等待一个完成再进行下一步,总执行时间将是 1+2+3+4 = 10 秒。
    • 确实如此。添加时间部分来证明这一点。
    【解决方案2】:

    实际上,使用for await 语法确实会立即触发所有承诺。

    一小段代码证明了这一点:

    const sleep = s => {
      return new Promise(resolve => {
        setTimeout(resolve, s * 1000);
      });
    }
    
    const somethingAsync = async t => {
      await sleep(t);
      return t;
    }
    
    (async () => {
      const items = [1, 2, 3, 4];
      const now = Date.now();
      for await (const res of items.map(e => somethingAsync(e))) {
        console.log(res);
      }
      console.log("time: ", (Date.now() - now) / 1000);
    })();
    

    标准输出: time: 4.001

    但循环的内部并不充当“回调”。如果我反转数组,所有日志都会立即出现。我想承诺会立即触发,运行时只是等待第一个解决方案进入下一次迭代。

    编辑:实际上,当我们将for await 与异步迭代器以外的东西一起使用时,使用Promise.all 是不好的做法,最好是使用Promise.all,根据@Bergi 在他的回答中。

    【讨论】:

      【解决方案3】:

      是的,它们完全不同。 for await 应该与异步迭代器一起使用,而不是与预先存在的承诺数组一起使用。

      只是为了说清楚,

      for await (const res of items.map(e => somethingAsync(e))) …
      

      工作原理与

      相同
      const promises = items.map(e => somethingAsync(e));
      for await (const res of promises) …
      

      const promises = [somethingAsync(items[0]), somethingAsync(items[1]), …];
      for await (const res of promises) …
      

      somethingAsync 呼叫立即发生,一次发生,在等待任何事情之前。然后,他们一个接一个被awaited,如果其中任何一个被拒绝,这绝对是一个问题:它会导致一个未处理的promise拒绝错误。 使用Promise.all 是处理一系列承诺的唯一可行选择

      for (const res of await Promise.all(promises)) …
      

      详情请参阅Waiting for more than one concurrent await operationAny difference between await Promise.all() and multiple await?

      【讨论】:

      • IMO 一组 promise 被同化为异步迭代器,因为它们将控制权交还给事件循环,对吧?如果在我的 somethingAsync 函数中,我正确地捕捉到了所有内容怎么办?
      • 是的,数组提供了一个异步迭代器,这就是循环不会引发协议错误的原因。但是该迭代一次只等待一个 promise,忽略了数组后面的 promise 的错误。当然,如果somethingAsync 从不出错,就不会发生任何坏事,但这很难保证。在一系列 Promise 上使用 for await 仍然是一种不好的做法。
      • 好的,将使用Promise.all,感谢您的澄清
      【解决方案4】:

      当在异步迭代器上当前迭代的计算依赖于之前的一些迭代时,就需要for await ...。如果没有依赖关系,Promise.all 是您的选择。 for await 构造被设计为与异步迭代器一起使用,尽管 - 在您的示例中,您可以将它与一组承诺一起使用。

      请参阅javascript.info 一书中的示例paginated data,了解使用无法使用Promise.all 重写的异步迭代器的示例:

      (async () => {
        for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {
          console.log(commit.author.login);
        }
      })();
      

      在这里,fetchCommits 异步迭代器向fetch 发出请求,提交 GitHub 存储库。 fetch 以 30 次提交的 JSON 响应,并在 Link 标头中提供指向下一页的链接。 因此下一次迭代只能在上一次迭代有下一个请求的链接后开始

      async function* fetchCommits(repo) {
        let url = `https://api.github.com/repos/${repo}/commits`;
      
        while (url) {
          const response = await fetch(url, { 
            headers: {'User-Agent': 'Our script'}, 
          });
      
          const body = await response.json(); // (array of commits
      
          // The URL of the next page is in the headers, extract it using a regexp
          let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
          nextPage = nextPage?.[1];
      
          url = nextPage;
      
          for(let commit of body) { // yield commits one by one, until the page ends
            yield commit;
          }
        }
      }
      

      【讨论】:

      • 非常清楚,谢谢。在您的代码示例中,for await 的行为类似于yield*,不是吗?因为每次迭代都会产生多个项目
      • 没有生成器组合,每次提交只有一个yield。每 30 次迭代后加载一个新页面,接下来的 30 次提交是yielded。真的迭代之间的依赖关系每三十次才发生一次
      猜你喜欢
      • 2020-11-01
      • 2021-03-21
      • 2018-10-27
      • 2020-04-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-02
      • 2020-06-23
      相关资源
      最近更新 更多