Асинхронный JavaScript: от Callback Hell к Async и Await
Опубликовано: 2022-03-11Одним из ключей к написанию успешного веб-приложения является возможность выполнять десятки вызовов AJAX на страницу.
Это типичная проблема асинхронного программирования, и то, как вы решите поступать с асинхронными вызовами, в значительной степени создаст или сломает ваше приложение и, соответственно, весь ваш стартап.
Синхронизация асинхронных задач в JavaScript долгое время была серьезной проблемой.
Эта проблема затрагивает как внутренних разработчиков, использующих Node.js, так и внешних разработчиков, использующих любую среду JavaScript. Асинхронное программирование является частью нашей повседневной работы, но к этой проблеме часто относятся легкомысленно и не решают ее в нужное время.
Краткая история асинхронного JavaScript
Первое и самое простое решение пришло в виде вложенных функций в качестве обратных вызовов . Это решение привело к тому, что называется адом обратных вызовов , и слишком многие приложения до сих пор ощущают на себе его ожог.
Затем мы получили обещания . Этот шаблон сделал код намного легче для чтения, но он был далек от принципа «Не повторяйся» (DRY). Было еще слишком много случаев, когда вам приходилось повторять одни и те же фрагменты кода, чтобы правильно управлять потоком приложения. Последнее дополнение в виде операторов async/await, наконец, сделало асинхронный код в JavaScript таким же простым для чтения и написания, как и любой другой фрагмент кода.
Давайте рассмотрим примеры каждого из этих решений и подумаем об эволюции асинхронного программирования в JavaScript.
Для этого рассмотрим простую задачу, выполняющую следующие шаги:
- Проверьте имя пользователя и пароль пользователя.
- Получить роли приложения для пользователя.
- Регистрировать время доступа к приложению для пользователя.
Подход 1: Callback Hell («Пирамида судьбы»)
Древнее решение для синхронизации этих вызовов заключалось в использовании вложенных обратных вызовов. Это был достойный подход для простых асинхронных задач JavaScript, но он не масштабировался из-за проблемы, называемой адом обратных вызовов.
Код для трех простых задач будет выглядеть примерно так:
const verifyUser = function(username, password, callback){ dataBase.verifyUser(username, password, (error, userInfo) => { if (error) { callback(error) }else{ dataBase.getRoles(username, (error, roles) => { if (error){ callback(error) }else { dataBase.logAccess(username, (error) => { if (error){ callback(error); }else{ callback(null, userInfo, roles); } }) } }) } }) };
Каждая функция получает аргумент, который является другой функцией, вызываемой с параметром, являющимся ответом на предыдущее действие.
Слишком много людей испытают замирание мозга, просто прочитав предложение выше. Наличие приложения с сотнями одинаковых блоков кода вызовет еще больше проблем у человека, поддерживающего код, даже если он написал его сам.
Этот пример становится еще более сложным, когда вы понимаете, что database.getRoles
— это еще одна функция, имеющая вложенные обратные вызовы.
const getRoles = function (username, callback){ database.connect((connection) => { connection.query('get roles sql', (result) => { callback(null, result); }) }); };
Помимо сложного в обслуживании кода, принцип DRY в данном случае абсолютно бесполезен. Обработка ошибок, например, повторяется в каждой функции, а основной обратный вызов вызывается из каждой вложенной функции.
Еще более сложной задачей являются более сложные асинхронные операции JavaScript, такие как зацикливание асинхронных вызовов. На самом деле, нет тривиального способа сделать это с обратными вызовами. Вот почему библиотеки JavaScript Promise, такие как Bluebird и Q, получили такое большое распространение. Они предоставляют способ выполнения общих операций с асинхронными запросами, которые сам язык еще не предоставляет.
Вот тут-то и появляются нативные промисы JavaScript.
Обещания JavaScript
Обещания были следующим логическим шагом в избавлении от ада обратных вызовов. Этот метод не избавил от использования обратных вызовов, но упростил цепочку функций и упростил код, облегчив его чтение.

При наличии промисов код в нашем примере с асинхронным JavaScript будет выглядеть примерно так:
const verifyUser = function(username, password) { database.verifyUser(username, password) .then(userInfo => dataBase.getRoles(userInfo)) .then(rolesInfo => dataBase.logAccess(rolesInfo)) .then(finalResult => { //do whatever the 'callback' would do }) .catch((err) => { //do whatever the error handler needs }); };
Для достижения такой простоты все функции, используемые в примере, должны быть промисифицированы . Давайте посмотрим, как обновить метод getRoles
, чтобы он возвращал Promise
:
const getRoles = function (username){ return new Promise((resolve, reject) => { database.connect((connection) => { connection.query('get roles sql', (result) => { resolve(result); }) }); }); };
Мы изменили метод, чтобы он возвращал Promise
с двумя обратными вызовами, и сам Promise
выполняет действия из метода. Теперь обратные вызовы resolve
и reject
будут сопоставлены с Promise.then
и Promise.catch
соответственно.
Вы можете заметить, что метод getRoles
по-прежнему внутренне подвержен феномену пирамиды гибели. Это связано с тем, как создаются методы базы данных, поскольку они не возвращают Promise
. Если бы наши методы доступа к базе данных также возвращали Promise
, метод getRoles
выглядел бы следующим образом:
const getRoles = new function (userInfo) { return new Promise((resolve, reject) => { database.connect() .then((connection) => connection.query('get roles sql')) .then((result) => resolve(result)) .catch(reject) }); };
Подход 3: асинхронный/ожидание
Пирамида гибели была значительно смягчена введением Промисов. Однако нам по-прежнему приходилось полагаться на обратные вызовы, которые передаются методам .then
и .catch
объекта Promise
.
Обещания проложили путь к одному из самых крутых улучшений в JavaScript. ECMAScript 2017 добавил синтаксический сахар поверх промисов в JavaScript в виде операторов async
и await
.
Они позволяют нам писать код на основе Promise
так, как если бы он был синхронным, но без блокировки основного потока, как показано в этом примере кода:
const verifyUser = async function(username, password){ try { const userInfo = await dataBase.verifyUser(username, password); const rolesInfo = await dataBase.getRoles(userInfo); const logStatus = await dataBase.logAccess(userInfo); return userInfo; }catch (e){ //handle errors as needed } };
Ожидание разрешения Promise
разрешено только в async
функциях, что означает, что verifyUser
должен был быть определен с использованием async function
.
Однако после внесения этого небольшого изменения вы можете await
любого Promise
без дополнительных изменений в других методах.
Async — долгожданное выполнение обещания
Асинхронные функции — это следующий логический шаг в развитии асинхронного программирования в JavaScript. Они сделают ваш код намного чище и проще в обслуживании. Объявление функции как async
гарантирует, что она всегда возвращает Promise
, поэтому вам больше не нужно об этом беспокоиться.
Почему вы должны начать использовать async
функцию JavaScript сегодня?
- Полученный код намного чище.
- Обработка ошибок намного проще и зависит от
try
/catch
, как и в любом другом синхронном коде. - Отладка намного проще. Установка точки останова внутри блока
.then
не приведет к переходу к следующему блоку.then
, поскольку выполняется только синхронный код. Но вы можете выполнять вызовыawait
, как если бы они были синхронными вызовами.