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

Original author: Anton Lavrenov
  • Translation
  • 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 и некоторые другие задачи могуть быть выполнены параллельно.

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 33

  • UFO just landed and posted this here
      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.
                    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

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

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

                            По моему это мертворожденная бесполезная хрень. Не знаю не 1 применения когда стоит её применять, точнее всё случаи когда стоит лучше переписать по другому.

                              0

                              Как вы в таком случае предлагаете читать большой файл под нодой? Варианта как бы только два: либо for await, либо подписка на событие data.


                              Но в комментарии ниже вы написали, что вам не нравится комбинация }) — значит, остаётся только for await.

                                0

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

                            –1

                            Стоп, сувать в forEach анонимную функцию когда есть просто for имхо извращение. ForEach используют когда пишут в функциональном стиле.
                            Это не говоря о том что фор более гибкий, можно закешировать длину массива ускорив на 10% выполнение, можно через 1 перебирать и тд.
                            Да и потом чем мешьше вложеных скобок тем читабельнее код, вот от такого


                            });

                            нужно избавлсятся

                              0
                              А какие ещё варианты вы предлагаете для случая, когда итерации цикла должны быть параллельными?
                                0

                                Если нужно обработать момент когда всё итерации разрешатся, то вариант автора статьи единственный адекватный.

                                  0
                                  «Вариант автора статьи» — это который? Тут как бы приведено 3 разных варианта, каждый для своего случая.
                                    0
                                    когда итерации цикла должны быть параллельными

                                    Вы сами спросили про третий случай по нумерации автора, только через промис олл по другому это костыли типа while(1) и проверять всё итерации на предмет завершения.

                                      0
                                      Вариант 3 — это когда итерации должны быть параллельными и требуется отследить завершение. А если отслеживать завершение не требуется — он превращается в вариант 1.
                              0
                              async function processArray(array) {
                                array.forEach(async (item) => {
                                  await func(item);
                                })
                                console.log('Done!');
                              }

                              Огромное спасибо за статью, без нее тупил бы полдня, не понимая, почему не срабатывает await, у меня ровно такой случай!

                              Only users with full accounts can post comments. Log in, please.