- Как работать с async/await в циклах JavaScript
- Синхронные циклы
- Асинхронные циклы
- 1. Не дожидаться результата выполнения
- 2. Обработка цикла последовательно
- 3. Обработка цикла параллельно
- Поймут даже дети: простое объяснение async/await и промисов в JavaScript
- Что значит «асинхронный»?
- Пример из жизни
- Теперь давайте перейдем к JavaScript, хорошо?
- Что насчет явных промисов?
- Заключение
Как работать с async/await в циклах JavaScript
Как запустить асинхронные циклы по порядку или параллельно в JavaScript?
Перед тем, как делать асинхронную магию, я хочу напомнить как выглядят классические синхронные циклы.
Синхронные циклы
Очень давно я писал циклы таким способом (возможно вы тоже):
Этот цикл хороший и быстрый. Но у него много проблем с читаемостью и с поддержкой. Через некоторое время я привык к его лучшей версии:
Язык 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]);
В некоторых ситуация это может быть нормальным результатом. Но всё же в большинстве вариантов это не подходящая логика.
2. Обработка цикла последовательно
Чтобы дождаться результата выполнения тела цикла нам нужно вернуться к старому доброму циклу «for». Но в этот раз мы будем использовать его новую версию с конструкцией for..of (Спасибо Iteration Protocol):
async function processArray(array) < for (const item of array) < await delayedLog(item); >console.log('Done!'); >
Это даст нам ожидаемый результат:
Каждый элемент массива будет обработан последовательно. Но мы можем запустить цикл параллельно!
3. Обработка цикла параллельно
Нужно слегка изменить код, чтобы запустить операции параллельно:
async function processArray(array) < // делаем "map" массива в промисы const promises = array.map(delayedLog); // ждем когда всё промисы будут выполнены await Promise.all(promises); console.log('Done!'); >
Этот код может запустить несколько delayLog задач параллельно. Но будьте аккуратны с большими массивами. Слишком много задач может быть слишком тяжело для CPU и памяти.
Так же, пожалуйста, не путайте «параллельные задачи» из примера с реальной параллельностью и потоками. Этот код не гарантирует параллельного исполнения. Всё завесит от тела цикла (в примере это delayedLog ). Запросы сети, webworkers и некоторые другие задачи могуть быть выполнены параллельно.
Поймут даже дети: простое объяснение async/await и промисов в JavaScript
Привет, Хабр! Представляю вашему вниманию перевод статьи «JavaScript Async/Await and Promises: Explained like you’re five years old» автора Jack Pordi.
Каждый, кто считает себя JavaScript-разработчиком, в какой-то момент должен был столкнуться с callback-функциями, промисами или, с недавних пор, с синтаксисом async/await. Если вы пробыли в игре достаточно долго, вы, вероятно, застали времена, когда вложенные callback-функции были единственным способом достижения асинхронности в JavaScript.
Когда я начал изучать и писать на JavaScript, уже существовало миллиард руководств и туториалов, объясняющих, как добиться асинхронности в JavaScript. Тем не менее, многие из них просто объясняли, как преобразовать callback-функции в промисы или промисы в async/await. Для многих этого, вероятно, более чем достаточно, чтобы они «поладили» с ними и начали использовать их в своем коде.
Однако если вы, как я, действительно хотите понять асинхронное программирование (а не только синтаксис JavaScript!), то, возможно, вы согласитесь со мной, что существует нехватка материалов, объясняющих асинхронное программирование фактически с нуля.
Что значит «асинхронный»?
Как правило, задавая этот вопрос, вы можете услышать что-то из следующего:
- Существует несколько потоков, которые выполняют код одновременно.
- Более чем один фрагмент кода выполняется одновременно.
- Это параллелизм.
Пример из жизни
Представьте, что вы готовите овощной суп. Для хорошей и простой аналогии предположим, что овощной суп состоит только из лука и моркови. Рецепт такого супа может быть следующим:
- Нарежьте морковь.
- Нарежьте лук.
- Добавьте воду в кастрюлю, включите плиту и подождите, пока она закипит.
- Добавьте морковь в кастрюлю и оставьте на 5 минут.
- Добавьте лук в кастрюлю и варите еще 10 минут.
- Шаги 3, 4 и 5 фактически не требуют от вас как от шеф-повара ничего делать, кроме как наблюдать за процессом и следить за временем.
- Шаги 1 и 2 требуют от вас, чтобы вы активно что-то делали.
- Начните кипятить кастрюлю с водой.
- Пока ждете, когда кастрюля закипит, начните резать морковь.
- К тому времени, когда вы закончите измельчать морковь, вода должна закипеть, поэтому добавьте морковь.
- Пока морковь готовится в кастрюле, нарежьте лук.
- Добавьте лук и готовьте еще 10 минут.
Теперь давайте перейдем к JavaScript, хорошо?
Итак, придерживаясь того же примера овощного супа, я напишу несколько функций для представления шагов рецепта, описанных выше.
Сначала давайте напишем синхронные функции, которые представляют задачи, не требующие ожидания времени. Это старые добрые функции JavaScript, но обратите внимание, что я описал функции chopCarrots и chopOnions как задачи, требующие активной работы (и времени), позволяя им выполнять некоторые длинные вычисления. Полный код доступен в конце статьи [1].
function chopCarrots() < /* Тут длинные вычисления. */ console.log("Морковь нарезана!"); >function chopOnions() < /* Тут длинные вычисления. */ console.log("Лук нарезан!"); >function addOnions() < console.log("Лук в кастрюлю добавлен!"); >function addCarrots()
Перед тем, как перейти к асинхронным функциям, сначала я быстро объясню, как система типов JavaScript обрабатывает асинхронность: в основном все результаты (включая ошибки) асинхронных операций должны быть обернуты в промис(ы).
Чтобы функция возвращала промис, вы можете:
- явно вернуть промис, т.е. return new Promise(…) ;
- неявно вернуть промис – добавить ключевое слово async к объявлению функции, т.е. async function foo() ;
- использовать оба варианта.
Итак, наши асинхронные функции, представляющие шаги 3–5 приготовления овощного супа, выглядят следующим образом:
async function letPotKeepBoiling(time) < return; // Возвращаем промис, чтобы кастрюля кипела определенное время >async function boilPot() < return; // Возвращаем промис, чтобы довести суп до состояния кипения >
Ещё раз, я удалил детали реализации, чтобы на них не отвлекаться, но они опубликованы в конце статьи [1].
Важно знать, что для того, чтобы дождаться результата промиса, чтобы потом можно было с ним что-то делать, вы можете просто использовать ключевое слово await :
async function asyncFunction() < /* Возвращаем промис. */ >result = await asyncFunction();
Итак, теперь нам просто нужно собрать все это вместе:
function makeSoup() < const pot = boilPot(); chopCarrots(); chopOnions(); await pot; addCarrots(); await letPotKeepBoiling(5); addOnions(); await letPotKeepBoiling(10); console.log("Ваш овощной суп готов!"); >makeSoup();
Но подождите! Это не работает! Вы увидите ошибку SyntaxError: await is only valid in async functions . Почему? Потому что, если вы не объявляете функцию с помощью ключевого слова async , то по умолчанию JavaScript определяет ее как синхронную функцию – а синхронный означает отсутствие ожидания! [3]. Это также значит, что вы не можете использовать await за пределами функции.
Следовательно, мы просто добавляем ключевое слово async в функцию makeSoup :
async function makeSoup() < const pot = boilPot(); chopCarrots(); chopOnions(); await pot; addCarrots(); await letPotKeepBoiling(5); addOnions(); await letPotKeepBoiling(10); console.log("Ваш овощной суп готов!"); >makeSoup();
И вуаля! Обратите внимание, что во второй строке я вызываю асинхронную функцию boilPot без ключевого слова await , потому что мы не хотим ждать, пока кастрюля закипит, прежде чем начать резать морковь. Мы ожидаем только промис pot в пятой строке, прежде чем нам нужно будет положить морковь в кастрюлю, потому что мы не хотим делать это до того, как вода закипит.
Что происходит во время вызовов await ? Ну, ничего… вроде…
В контексте функции makeSoup вы можете просто думать о ней как о том, что вы ожидаете, что что-то произойдет (или результат, который в конечном итоге будет возвращен).
Но помните: вы (как и ваш процессор) никогда не захотите просто сидеть и ждать чего-то, в то время как можно потратить свое время на другие дела.
Следовательно, вместо того, чтобы только готовить суп, мы могли бы параллельно готовить что-то еще:
Пока мы ожидаем letPotKeepBoiling , мы можем, например, готовить пасту.
Видите? Синтаксис async/await на самом деле довольно прост в использовании, если вы его понимаете, согласны?
Что насчет явных промисов?
Хорошо, если вы настаиваете, я перейду к использованию явных промисов (прим. перев.: под явными промисами автор подразумевает непосредственно сам синтаксис промисов, а под неявными промисами – синтаксис async/await, т.к. он возвращает промис неявно – не нужно писать return new Promise(…) ). Имейте в виду, что методы async/await основаны на самих промисах и, следовательно, оба варианта полностью совместимы.
Явные промисы, на мой взгляд, находятся между callback-функциями старого стиля и новым сексуальным синтаксисом async/await. В качестве альтернативы, вы также можете думать о сексуальном синтаксисе async/await как о не более чем неявных промисах. В конце концов, конструкция async/await пришла после промисов, которые, в свою очередь, пришли после callback-функций.
Воспользуйтесь нашей машиной времени, чтобы переместиться в «ад обратных вызовов» (callback hell) [4]:
function callbackHell() < boilPot( () => < addCarrots(); letPotKeepBoiling(() => < addOnions(); letPotKeepBoiling(() =>< console.log("Ваш овощной суп готов!"); >, 1000); >, 5000); >, 5000, chopCarrots(), chopOnions() ); >
Я не собираюсь лгать, я написал этот пример на лету, когда работал над этой статьей, и это заняло у меня гораздо больше времени, чем я хотел бы признать. Многие из вас, возможно, даже не будут знать, что вообще тут происходит. Мой дорогой друг, разве все эти callback-функции не ужасны? Пусть это будет уроком, чтобы никогда больше не использовать callback-функции.
И, как и обещал, тот же пример с явными промисами:
function makeSoup() < return Promise.all([ new Promise((reject, resolve) =>< chopCarrots(); chopOnions(); resolve(); >), boilPot() ]) .then(() => < addCarrots(); return letPotKeepBoiling(5); >) .then(() => < addOnions(); return letPotKeepBoiling(10); >) .then(() => < console.log("Ваш овощной суп готов!"); >); >
Как видите, промисы все еще похожи на callback-функции.
Я не буду вдаваться в подробности, но главное:
- .then — это метод промиса, который берет его результат и передает его в функцию аргумента (по сути, в callback-функцию…)
- Вы никогда не сможете использовать результат промиса вне контекста .then . По сути, .then похож на асинхронный блок, который ожидает результат, а затем передает его в callback-функцию.
- Помимо метода .then , в промисах существует еще один метод — .catch . Он нужен для обработки ошибок в промисах. Но я не буду вдаваться в детали, потому что на эту тему уже есть миллиард статей и туториалов.
Заключение
Я надеюсь, что вы получили некоторое представление о промисах и асинхронном программировании из этой статьи или, возможно, хотя бы узнали о хорошем примере из жизни, чтобы объяснить это кому-то еще.
Итак, какой из способов вам использовать: промисы или async/await?
Ответ полностью зависит от вас — и я бы сказал, что совмещать их не так уж и плохо, так как оба подхода полностью совместимы друг с другом.
Тем не менее, лично я нахожусь на 100% в лагере async/await, так как для меня код намного понятнее и лучше отражает истинную многозадачность асинхронного программирования.
[1]: Полный исходный код доступен здесь.
[2]: Оригинал статьи «Async function vs. a function that returns a Promise», перевод статьи «Разница между асинхронной функцией и функцией, возвращающей промис».
[3]: Вы можете утверждать, что JavaScript, вероятно, может определять тип async/await по телу функций и рекурсивно проверять, но JavaScript не был разработан для того, чтобы заботиться о безопасности статического типа во время компиляции, не говоря уже о том, что разработчикам намного удобнее явно видеть тип функции.
[4]: Я написал «асинхронные» функции, предполагая, что они работают под тем же интерфейсом, что и setTimeout . Обратите внимание, что callback-функции несовместимы с промисами и наоборот.