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