Углубляемся в JavaScript: всё ли может async/await, или когда использовать Promise
Разбираемся, что из себя представляют async/await и Promise, какие у них плюсы и минусы и что нужно использовать в зависимости от ситуации.
20К открытий21К показов
Игорь Яковлев
руководитель AFFINAGE
Что такое async/await и promise?
Прежде чем ответить на поставленный вопрос, нам необходимо узнать немного теории.
Асинхронность меняет сложившуюся парадигму последовательного кода. Последовательность — когда только одна конкретная операция происходит в данный момент времени. Если функция зависит от результата выполнения другой функции, то она должна дождаться пока прошлая функция не завершит свою работу. Для пользователя это значит состояние вечного «ждуна».
Асинхронность нужна нам, чтобы делать несколько операций и функций параллельно. Асинхронное программирование — это инструмент для оптимизации высоконагруженных сайтов с долгими и частыми ожиданиями обратной связи. Например, когда одна функция создаёт canvas на странице, другая функция может подготовить данные необходимые для отрисовки внутри canvas. Ещё пример, когда пользователь кладет товар в корзину ему не обязательно ждать ответа сервера, мы заранее можем показать анимацию добавления товара в корзину, а всю остальную логику проверок сделать после ответа сервера, не блокируя интерфейс пользователю.
На самом деле с точки зрения машинного кода, async/await и промисы это абсолютно то же самое. Но мы то с вами люди, и нам важен синтаксис. И разница в синтаксисе настолько существенна, что разделила разработчиков на два лагеря. Любители колбэков выбрали Promise, а не любители цепочек выбрали async/await.
Async/await — синтаксис работающий с промисами, придуман как альтернатива синтаксису промисов. Используя async, можно полностью избежать использования цепочек промисов с помощью await. Async создает Promise. А await ждет выполнения промиса.
Promise — обертка (класс, для простоты понимания) для отложенных и асинхронных вычислений. Ожидает выполнения колбэк функций и никак иначе. Есть два колбэка: один заявляет об успешном выполнении, другой об ошибке. Promise может находиться в трёх состояниях: ожидание (pending), исполнено (fulfilled), отклонено (rejected). Промис начинает выполняться когда мы вызываем метод .then
.
Давайте посмотрим практические маленькие примеры синтаксиса.
Пример 1:
Пример 2:
Пример 3:
Так как async
является надстройкой над промисами, то мы можем смешивать код, например так:
или так
Плюсы и минусы в теории
Async/await
Плюсы
- Удобство и простота чтения
- Возможность использования последовательного стиля программирования
Минусы
- Легко наткнуться на избыточное ожидание последовательного кода. Для истинной параллельности нужно модифицировать код.
- Неочевидность возвращаемых значений try…catch.
Promise
Плюсы
- Использует традиционный подход колбэков.
- Данные с ошибками и данные с успешным результатом операции однозначно понимаемы.
- Возможность использовать Promise.all без оглядки на синтаксис.
- Оповещения Promise.resolve и Promise.reject доступны везде.
- Наглядное использование метода Promise.finally.
Минусы
- При неправильном использовании возможно создание слишком глубоких использований цепочек
.then
На примерах выше видно, что Promise субъективно является более чистым кодом. Более того я заранее заложил одну противную пакость в примерах, о которой расскажу позже. Эта особенность не позволяет выбранному нами синтаксису использовать асинхронность в полной мере. Кто её нашел сходу, может дальше не читать ?
Вера в обещание
Мое знакомство с асинхронным js-кодом началось с библиотеки «КриптоПро ЭЦП Browser plug-in». Те, кто сталкивался с данной библиотекой, должны меня понять, у меня не было выбора, я искренне влюбился в промисы ? Она вся утыкана промисами. И первой техникой, которой пришлось овладеть, были .then
и .catch
. Порой вложенность кода составляла 10-15 уровней .then
. Спустя годы я понимаю почему разработчикам плагина пришлось так поступить, но она прекрасна в своей ужасности.
Шло время, навыки оттачивались, и с тех пор я всегда пишу js-код на промисах.
А теперь о пакости.
Сложный кейс с промисами, и главное преимущество промисов — колбеки
Попалась мне интереснейшая задача «Платежная система отвечает об успешной оплате не сразу, поэтому придется слать несколько запросов в течение 30 секунд, при этом держать пользователя в режиме прелоадера, при этом если оплата пройдет раньше чем 30 секунд, то из цикла нужно выйти и отключить прелоадер, и если за 30 секунд ответа не получено, то показать ошибку».
Архитектура:
- Интервал запросов к серверу 1 секунда.
- Необходим один большой (глобальный) промис, чтобы было удобно отключить прелоадер.
- До входа в асинхронный код нужно включить прелоадер.
- Внутри асинхронного кода должно произойти «нечто ужасное» без потери читабельности.
- Отключение прелоадера должно происходить в финальном коде, независимо от того, успешная оплата, или ошибка, и независимо от того, сколько промисов будет использоваться в асинхронном коде.
Для удобства сопоставления алгоритма и архитектуры код совсем чуть-чуть упрощен, и совпадает с оригинальным на 90%.
Результат:
На данном примере видно как используются колбэки — у нас есть полный простор в передаче ошибок в родительский промис, их множественный вызов в разных местах, когда нам необходимо. И также максимальный простор для выбора момента уведомления «родителя» об успешном окончании, тем самым мы решаем 5 пункт из запланированной архитектуры.
Требование заказчика
История закончилась бы замечательно, если бы не одно «но». ТЗ требовало, чтобы весь код был написан на async/await. Глядя на код выше можно сказать, что это достаточно сложный кейс. Первое, что можно подумать: «Это невозможно! Ведь async/await не могут ждать колбека, они только выполняют код и ничего не ждут.»
Ну хорошо… требование заказчика — закон… переписываем.
Задача действительно оказалась не решаема на уровне async/await. Потому что async
, await
и timeout
не работают в связке. Пришлось совсем чуть-чуть смешать два разных синтаксиса с помощью функции sleep
. Хорошо это или плохо? Вопрос субъективный. Мы лишь в очередной раз убедились, что async/await является лишь надстройкой над промисами.
Вывод
Нет ничего хуже, чем смешение разных стилей написания кода на одном проекте. Поэтому выбирайте стайлгайд по асинхронному коду заранее. Описанный выше кейс — это редкость. И зачастую async/await будет достаточно. Но если вы чувствуете, что на проекте будут сложные кейсы и есть вероятность использования колбэков, то используйте изначально промисы, применение конструкторов Promise тоже редкость. Остальное дело вкуса.
20К открытий21К показов