JavaScript

Подводные камни 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
Editorial Team

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

12 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

12 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

Когда и как выбирать между медиа запросами и контейнерными запросами

Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…

2 года ago