等待 VS Promise.all

Posted

技术标签:

【中文标题】等待 VS Promise.all【英文标题】:for await of 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 都是同时触发的,而循环内部对它们来说就像一个回调?

【问题讨论】:

【参考方案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 operation 和Any 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。真的迭代之间的依赖关系每三十次才发生一次

以上是关于等待 VS Promise.all的主要内容,如果未能解决你的问题,请参考以下文章

处理多个承诺拒绝[重复]

Promise.all和Promise.race的区别和使用

隐式 vs 显式 vs Fluent 等待

csharp Async Vs等待

等待 VS Promise.all

VS2005工具箱里没有ToolStripPanel控件,在哪里添加?在线等待,谢谢各位了