Подводные камни Async/Await в циклах массива

Spread the love

Использование async/await при переборе массивов в цикле Javascript кажется простым, но есть некоторые неинтуитивные особенности, на которые следует обратить внимание при их использование. Давайте рассмотрим три разных примера, чтобы понять, на что следует обращать внимание и какой цикл лучше всего подходит для конкретных случаев использования.

forEach

Если вы вынесете из этой статьи только одну вещь, то пусть это будет то что async/await не работает в Array.prototype.forEach. Давайте рассмотрим пример, чтобы понять, почему:

const urls = [
  'https://jsonplaceholder.typicode.com/todos/1',
  'https://jsonplaceholder.typicode.com/todos/2',
  'https://jsonplaceholder.typicode.com/todos/3'
];

async function getTodos() {
  await urls.forEach(async (url, idx) => { 
    const todo = await fetch(url);
    console.log(`Received Todo ${idx+1}:`, todo);
  });
  
  console.log('Finished!');
}

getTodos();
Finished!
Received Todo 2, Response: { ··· }
Received Todo 1, Response: { ··· }
Received Todo 3, Response: { ··· }

⚠️ Проблема 1:

Приведенный выше код будет успешно выполнен. Однако обратите внимание, что фраза Finished! оказалась первым, несмотря на то, что мы использовали await перед urls.forEach. Первая проблема заключается в том, что вы не можете ожидать окончания всего цикла при использовании forEach.

⚠️ ️Проблема 2:

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

По обеим этим причинам на forEach нельзя полагаться, если вы используете async/await.

Promise.all

Давайте решим проблему ожидания завершения всего цикла. Поскольку операция await создает promise под капотом, то мы можем использовать Promise.all для ожидания завершения всех запросов, которые были отклонены во время цикла:

const urls = [
  'https://jsonplaceholder.typicode.com/todos/1',
  'https://jsonplaceholder.typicode.com/todos/2',
  'https://jsonplaceholder.typicode.com/todos/3'
];

async function getTodos() {
  const promises = urls.map(async (url, idx) => 
    console.log(`Received Todo ${idx+1}:`, await fetch(url))
  );

  await Promise.all(promises);

  console.log('Finished!');
}

getTodos();
Received Todo 1, Response: { ··· }
Received Todo 2, Response: { ··· }
Received Todo 3, Response: { ··· }
Finished!

Мы решили проблему ожидания завершения каждого запроса, прежде чем продолжить работу. Также кажется, что мы решили проблему с запросами, поступающими не по порядку, но это не совсем так.

Как упоминалось ранее, Promise.all выполняет все переданные ему промисы параллельно (асинхронно). Он не будет ждать возврата первого запроса перед выполнением второго или третьего запроса. Для большинства целей это нормально, и это очень эффективное решение. Но если вам действительно нужно, чтобы каждый запрос выполнялся по порядку (то есть синхронно), Promise.all не решит эту проблему.

for … of

Теперь мы знаем, что forEach вообще не учитывает async/await, а Promise.all работает только в том случае, если порядок выполнения не имеет значения. Давайте посмотрим на решение, которое подходит для обоих случаев.

Цикл for…of выполняется в ожидаемом порядке — ожидает завершения каждой предыдущей операции await перед переходом к следующей:

const urls = [
  'https://jsonplaceholder.typicode.com/todos/1',
  'https://jsonplaceholder.typicode.com/todos/2',
  'https://jsonplaceholder.typicode.com/todos/3'
];

async function getTodos() {
  for (const [idx, url] of urls.entries()) {
    const todo = await fetch(url);
    console.log(`Received Todo ${idx+1}:`, todo);
  }

  console.log('Finished!');
}

getTodos();
Received Todo 1, Response: { ··· }
Received Todo 2, Response: { ··· }
Received Todo 3, Response: { ··· }
Finished!

Мне особенно нравится, как этот метод позволяет коду оставаться лаконичным — это одно из ключевых преимуществ использования async/await. Я считаю, что это намного легче читать, чем альтернативы.

Если вам не нужен доступ к индексу, код становится еще более лаконичным:

for (const url of urls) { ··· }

Одним из основных недостатков использования цикла for…of является низкая производительность по сравнению с другими вариантами циклов в Javascript. Однако аргументом производительности можно пренебречь при использовании его для ожидания (await) асинхронных вызовов, поскольку цель состоит в том, чтобы удерживать цикл до тех пор, пока каждый вызов не будет разрешен. Обычно я использую for…of, только если важен порядок выполнения.

Примечание. Вы также можете использовать базовые циклы for, чтобы получить все преимущества for…of, но мне нравится простота и читабельность, которые дает for…of.

Перевод: The Pitfalls of Async/Await in Array Loops

Была ли вам полезна эта статья?
[3 / 3.7]

Spread the love
Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments