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

https://lavrton.com/javascript-loops-how-to-handle-async-await-6252dd3c795/
  • Перевод
  • Tutorial

Как запустить асинхронные циклы по порядку или параллельно в 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 delayedLog(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 и некоторые другие задачи могуть быть выполнены параллельно.

Поделиться публикацией

Похожие публикации

Комментарии 23

    +1
    +, хотя, конечно, параллельный запуск с негарантированным порядком исполнения (не уверен) сложно назвать циклом :)
      0
      ну а результаты Promise.all где?
        0

        В данном примере результов нет, так как delayedLog просто выводил в консоль. Но если нужны то:


        const results = await Promise.all(array.map(delayedLog););
          +1
          Как получить список ошибок из отклонённых промисов?
            +1
            После первого же отклоненного промиса, выполнение приостанавливается. Ошибку вы сможете перехватить через try catch.
              0
              А как быть, если хочется, чтобы все промисы выполнились даже если один реджектнулся?

              Я для этого писал функцию promiseResolveAll, но может быть, есть более хороший способ.

              async function promiseResolveAll(promises){
                  if(promises.length === 0){
                      return [];
                  }
                  return new Promise(resolve => {
                      var results = [];
                      var processed = 0;
                      function onPromiseAnswer(type, i){
                          return value => {
                              results[i] = type ? {ok: true, value: value} : {ok: false, error: value};
                              if(++processed === promises.length){
                                  resolve(results);
                              }
                          };
                      }
              
                      promises.forEach((promise, i) => {
                          promise.then(onPromiseAnswer(true, i)).catch(onPromiseAnswer(false, i));
                      });
                  });
              }
                +4
                Я бы ваш алгоритм как-то так реализовал бы:

                const promises = [
                  Promise.resolve(1),
                  Promise.reject('something wrong'),
                  Promise.resolve(3),
                ];
                
                function fail(error) { 
                  return {ok: false, error};
                };
                function success(value) { 
                  return {ok: true, value};
                };
                
                const results = await Promise.all(
                  promises.map(p => p.then(success).catch(fail))
                );
                // [{ok: true, value: 1}, {ok: false, error: "something wrong"}, {ok: true, value: 3}];
                
                  0

                  Только всё-таки p => p.then(success, fail). Это и короче, и семантичнее (мы перехватываем не общую ошибку цепочки p.then(success), а ошибку которая возникает в p).

                  0
                  Есть предложение для будущей реализации: github.com/tc39/proposal-promise-allSettled.
                  0
                  Каким это таким образом выполнение приостанавливается, когда все промисы — независимые?
                    0
                    Согласен, ввожу в заблуждение.

                    Само по себе выполнение каждого из промисов продолжается, завершается лишь исполнение Promise.all() при первом же режекте.

                    Примерно проследить за поведением Promise.all() можно на этом примере:
                    'use strict';
                    
                    const sleep = (time, v, err = false) => new Promise((resolve, reject) => {
                        setTimeout(() => {
                            console.log(`start   ${v}, time ${time}`);
                            if (err) {
                                console.log(`reject  ${v}, time ${time}`);
                                return void reject(new Error(`REJECTED: ${v}`));
                            }
                    
                            console.log(`resolve ${v}, time ${time}`);
                            resolve(`RESOLVED ${v}`);
                        }, time * 1000);
                    });
                    
                    
                    const promises = [
                        sleep(2, '1'),
                        sleep(1, '2'),
                        sleep(2, '3'),
                        sleep(4, '4', true),
                        sleep(3, '5'),
                        sleep(2, '6'),
                        sleep(1, '7', true),
                        sleep(5, '8'),
                    ];
                    
                    
                    console.log('Begin');
                    Promise.all(promises)
                        .then(values => {
                            console.log(JSON.stringify(values, null, 4));
                        })
                        .catch(err => {
                            console.log(err);
                        })
                        .then(() => {
                            console.log('* Promise.all finished *');
                        });
                    console.log('End');
                    


                    Результат исполнения будет примерно таким:
                    % node test.js
                    Begin
                    End
                    start   2, time 1
                    resolve 2, time 1
                    start   7, time 1
                    reject  7, time 1
                    Error: REJECTED: 7
                    * Promise.all finished *
                    start   1, time 2
                    resolve 1, time 2
                    start   3, time 2
                    resolve 3, time 2
                    start   6, time 2
                    resolve 6, time 2
                    start   5, time 3
                    resolve 5, time 3
                    start   4, time 4
                    reject  4, time 4
                    start   8, time 5
                    resolve 8, time 5
                    
                  0
                  const results = await Promise.all(
                      array
                        .map(delayedLog)
                        .map(promise => promise.catch(err => new SynteticError(e))));
                  
                  const withError = result.filter(e => e instanceof SynteticError);

                  Некрасиво, да. try… catch тоже перестанет работать, потому что все ошибки будут обработаны. Но можно сделать обертку вроде promise.Any, который будет кидать массив exception.

              0
              await array.reduce((accumulator, item) => accumulator.then(item)), Promise.resolve());
              
                +2
                Вот не надо писать такого в туториалах, кто-то же может и запомнить!

                Трюк с reduce имел смысл только до появления await.
                  –1
                    0
                    По вашей ссылке написано, что следует избегать await в цикле, потому что цикл от такого становится последовательным, хотя мог бы быть параллельным.

                    Но reduce точно так же делает все итерации последовательными.
                +1
                github.com/sindresorhus/p-filter
                github.com/sindresorhus/p-map
                И другие от этого автора полезны тем, что позволяют указать concurrency — число одновременно обрабатываемых элементов массива.
                  +2
                  Не понятно, в чем проблема использовать классический for, который, так случилось, с await'ами заходит лучше всего.
                  for (var i=0; i < array.length; i++) {
                      await func(array[i])
                  }


                  Не модно? Смузями заплюют? Замените var на let — пусть подавятся.
                    0

                    Абсолютно согласен. Если хочется быть модным-функциональным можно последовательно в редьюсе обрабатывать промисы

                      +3
                      Это то же самое, что пример под номером 2 в статье, только с дополнительной переменной. Если вам нужна последовательная обработка каждого элемента массива, то используйте
                        –1
                        –1

                        помойму тема не раскрыта, где собственно for await

                          –1
                          Поддерживаю. Без for await статья не полна

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое