для ожидания VS Promise.all

Есть ли разница между этим:

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
}

Я знаю, что в первом фрагменте все обещания запускаются одновременно, но я не уверен насчет второго. Ожидает ли цикл for, пока будет выполнена первая итерация, для вызова следующего обещания? Или все обещания запускаются одновременно, а внутренняя часть цикла действует для них как обратный вызов?

4 ответа

Решение

Да, они совершенно разные. 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.all- единственный жизнеспособный выбор для работы с массивом обещаний:

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

См. Раздел " Ожидание более одной одновременной операции ожидания" и " Есть ли разница между await Promise.all() и несколькими операциями ожидания?" для подробностей.

Нужда в for await ...возникает, когда на асинхронном итераторе вычисление текущей итерации зависит от некоторых предыдущих итераций. Если нет зависимостей, это ваш выбор. for awaitконструкция была разработана для работы с асинхронными итераторами, хотя, как и в вашем примере, вы можете использовать ее с массивом промисов.

См. пример данных с разбивкой на страницы в книге javascript.info для примера использования асинхронного итератора, который нельзя переписать с помощью Promise.all:

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

Здесь fetchCommitsасинхронный итератор делает запрос к коммитам репозитория GitHub. fetchотвечает JSON из 30 коммитов, а также предоставляет ссылку на следующую страницу в 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;
    }
  }
}

Как вы сказали 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 синтаксис запускает все обещания сразу.

Небольшой фрагмент кода доказывает это:

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);
})();

stdout:time: 4.001

Но внутренняя часть цикла не действует "как обратный вызов". Если я переверну массив, все журналы появятся сразу. Я полагаю, что промисы запускаются сразу, а среда выполнения просто ждет разрешения первого, чтобы перейти к следующей итерации.

РЕДАКТИРОВАТЬ: На самом деле, используя for await это плохая практика, когда мы используем его с чем-то другим, кроме асинхронного итератора, лучше всего использовать Promise.all, согласно @Bergi в своем ответе.

Другие вопросы по тегам