Как запустить асинхронные циклы по порядку или параллельно в 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 и некоторые другие задачи могуть быть выполнены параллельно.
Комментарии (18)
zag2art
04.01.2019 20:51ну а результаты Promise.all где?
lavrton Автор
04.01.2019 20:54В данном примере результов нет, так как
delayedLog
просто выводил в консоль. Но если нужны то:
const results = await Promise.all(array.map(delayedLog););
Enverest
04.01.2019 00:22Как получить список ошибок из отклонённых промисов?
Kolonist
04.01.2019 13:00+1После первого же отклоненного промиса, выполнение приостанавливается. Ошибку вы сможете перехватить через
try catch
.Keyten
04.01.2019 15:35А как быть, если хочется, чтобы все промисы выполнились даже если один реджектнулся?
Я для этого писал функцию 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)); }); }); }
ilyapirogov
04.01.2019 17:11+2Я бы ваш алгоритм как-то так реализовал бы:
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}];
mayorovp
05.01.2019 13:16Только всё-таки
p => p.then(success, fail)
. Это и короче, и семантичнее (мы перехватываем не общую ошибку цепочкиp.then(success)
, а ошибку которая возникает вp
).
Serator
04.01.2019 18:07Есть предложение для будущей реализации: github.com/tc39/proposal-promise-allSettled.
mayorovp
05.01.2019 13:13Каким это таким образом выполнение приостанавливается, когда все промисы — независимые?
Kolonist
05.01.2019 14:04Согласен, ввожу в заблуждение.
Само по себе выполнение каждого из промисов продолжается, завершается лишь исполнение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
kahi4
04.01.2019 13:23const 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.
Taraflex
04.01.2019 03:12github.com/sindresorhus/p-filter
github.com/sindresorhus/p-map
И другие от этого автора полезны тем, что позволяют указать concurrency — число одновременно обрабатываемых элементов массива.
fukkit
04.01.2019 17:48+2Не понятно, в чем проблема использовать классический for, который, так случилось, с await'ами заходит лучше всего.
for (var i=0; i < array.length; i++) { await func(array[i]) }
Не модно? Смузями заплюют? Замените var на let — пусть подавятся.vpotseluyko
04.01.2019 18:52Абсолютно согласен. Если хочется быть модным-функциональным можно последовательно в редьюсе обрабатывать промисы
bill876
04.01.2019 20:09+2Это то же самое, что пример под номером 2 в статье, только с дополнительной переменной. Если вам нужна последовательная обработка каждого элемента массива, то используйте
epishman
+, хотя, конечно, параллельный запуск с негарантированным порядком исполнения (не уверен) сложно назвать циклом :)