Как работать с async-await в циклах JavaScript

в 17:09, , рубрики: asynchronous, await/async, javascript

Как запустить асинхронные циклы по порядку или параллельно в JavaScript?

Перед тем, как делать асинхронную магию, я хочу напомнить как выглядят классические синхронные циклы.

Синхронные циклы

Очень давно я писал циклы таким способом (возможно вы тоже):

for (var i=0; i < array.length; i++) {
  var item = array[i];
  // делаем что-нибудь с item
}

Этот цикл хороший и быстрый. Но у него много проблем с читаемостью и с поддержкой. Через некоторое время я привык его лучшей версии:

array.forEach((item) => {
  // делаем что-нибудь с item
});

Язык JavaScript развивается очень быстро. Появляются новые фичи и синтаксис. Одна из моих любых улучшений это async/await.

Сейчас я использую этот синтакс достаточно часто. И иногда встречаются ситуации, когда мне нужно что-либо сделать с элементами массива асинхронно.

Асинхронные циклы

Как использовать await в теле циклы? Давайте просто попробуем написать асинхронную функцию и ожидать задачу обработки каждого элемента:

async function processArray(array) {
  array.forEach(item => {
    // тут мы определили синхронную анонимную функцию
    // НО ЭТО КОД ВЫДАСТ ОШИБКУ!
    await func(item);
  })
}

Этот код выдаст ошибку. Почему? Потому что мы не можем использовать await внутри синхронной функции. Как вы можете видеть processArray — это асинхронная функция. Но анонимная функция, которую мы используем для forEach, является синхронной.

Что можно с этим сделать?

1. Не дожидаться результата выполнения

Мы можем определить анонимную функцию как асинхронную:

async function processArray(array) {
  array.forEach(async (item) => {
    await func(item);
  })
  console.log('Done!');
}

Но forEach не будет дожидаться выполнения завершения задачи. forEach — синхронная операция. Она просто запустить задачи и пойдет дальше. Проверим на простом тесте:

function delay() {
  return new Promise(resolve => setTimeout(resolve, 300));
}

async function delayedLog(item) {
  // мы можем использовать await для Promise
  // который возвращается из delay
  await delay();
  console.log(item);
}
async function processArray(array) {
  array.forEach(async (item) => {
    await func(item);
  })
  console.log('Done!');
}

processArray([1, 2, 3]);

В консоли мы увидим:

Done!
1
2
3

В некоторых ситуация это может быть нормальным результатом. Но всё же в большинстве вариантов это не подходящая логика.

2. Обработка цикла последовательно

Чтобы дождаться результата выполнения тела цикла нам нужно вернуться к старому доброму циклу "for". Но в этот раз мы будем использовать его новую версию с конструкцией for..of (Спасибо Iteration Protocol):

async function processArray(array) {
  for (const item of array) {
    await delayedLog(item);
  })
  console.log('Done!');
}

Это даст нам ожидаемый результат:

1
2
3
Done!

Каждый элемент массива будет обработан последовательно. Но мы может запустить цикл параллельно!

3. Обработка цикла параллельно

Нужно слегка изменить код, чтобы запустить операции параллельно:

async function processArray(array) {
  // делаем "map" массива в промисы
  const promises = array.map(delayedLog);
  // ждем когда всё промисы будут выполнены
  await Promise.all(promises);
  console.log('Done!');
}

Этот код запустить несколько delayLog задач параллельно. Но будьте аккуратны с большими массивами. Слишком много задач может быть слишком тяжело для CPU и памяти.

Так же, пожалуйста, не путайте "параллельные задачи" из примера с реальной параллельностью и потоками. Этот код не гарантирует параллельного исполнения. Всё завесит от тела цикла (в примере это delayedLog). Запросы сети, webworkers и некоторые другие задачи могуть быть выполнены параллельно.

Автор: Антон Лаврёнов

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js