В конце 2015 года я услышал об этой паре ключевых слов, которые ворвались в мир JavaScript, чтобы спасти нас от promise chain hell, который, в свою очередь, должен был спасти нас от callback hell. Давайте посмотрим несколько примеров, чтобы понять, как мы дошли до async/await.
Допустим, мы работаем над нашим API и должны отвечать на запросы серией асинхронных операций:
— проверить валидность пользователя
— собрать данные из базы данных
— получить данные от внешнего сервиса
— изменить и записать данные обратно в базу данных
Также давайте предположим, что мы не имеем каких-либо знаний о промисах, потому что мы путешествуем назад во времени и используем функции обратного вызова, чтобы обработать запрос. Решение выглядело бы примерно так:
function handleRequestCallbacks(req, res) {
var user = req.user
isUserValid(user, function (err) {
if (err) {
res.error('An error ocurred!')
return
}
getUserData(user, function (err, data) {
if (err) {
res.error('An error ocurred!')
return
}
getRate('service', function (err, rate) {
if (err) {
res.error('An error ocurred!')
return
}
const newData = updateData(data, rate)
updateUserData(user, newData, function (err, savedData) {
if (err) {
res.error('An error ocurred!')
return
}
res.send(savedData)
})
})
})
})
}
И это так называемый callback hell. Теперь вы знакомы с ним. Все его ненавидят, так как его трудно читать, отлаживать, изменять, он уходит все глубже и глубже во вложенности, обработка ошибок повторяется на каждому уровне и т.д.
Мы могли бы использовать знаменитую async библиотеку, чтобы немного очистить код. Код стал бы лучше, так как обработка ошибок, по крайней мере была бы в одном месте:
function handleRequestAsync(req, res) {
var user = req.user
async.waterfall([
async.apply(isUserValid, user),
async.apply(async.parallel, {
data: async.apply(getUserData, user),
rate: async.apply(getRate, 'service')
}),
function (results, callback) {
const newData = updateData(results.data, results.rate)
updateUserData(user, newData, callback)
}
], function (err, data) {
if (err) {
res.error('An error ocurred!')
return
}
res.send(data)
})
}
Позже мы узнали как использовать промисы и подумали, что мир больше не злится на нас, и мы почувствовали, что нужно провести рефакторинг кода еще раз, ведь все больше и больше библиотек также движется в мир промисов.
function handleRequestPromises(req, res) {
var user = req.user
isUserValidAsync(user).then(function () {
return Promise.all([
getUserDataAsync(user),
getRateAsync('service')
])
}).then(function (results) {
const newData = updateData(results[0], results[1])
return updateUserDataAsync(user, newData)
}).then(function (data) {
res.send(data)
}).catch(function () {
res.error('An error ocurred!')
})
}
Это гораздо лучше, чем раньше, гораздо короче и намного чище! Тем не менее возникло слишком много накладных расходов в виде множества then() вызовов, function () {...} блоков и необходимости добавлять несколько операторов return повсюду.
И наконец мы слышим о ES6, обо всех этих новых вещах, которые пришли в JavaScript, например стрелочные функции (и немного деструктуризации, чтобы было чуть веселее). Мы решаем дать нашему прекрасному коду еще один шанс.
function handleRequestArrows(req, res) {
const { user } = req
isUserValidAsync(user)
.then(() => Promise.all([getUserDataAsync(user), getRateAsync('service')]))
.then(([data, rate]) => updateUserDataAsync(user, updateData(data, rate)))
.then(data => res.send(data))
.catch(() => res.error('An error ocurred!'))
}
И вот оно! Этот обработчик запросов стал чистым, легкочитаемым. Мы понимаем что его легко изменить если нам нужно добавить, удалить или поменять местами что-то в потоке! Мы сформировали цепочку функций, которые одна за другой мутируют данные, которые мы собираем с помощью различных асинхронных операций. Мы не определяли промежуточные переменные для хранения этого состояния, а обработка ошибок находится в одном понятном месте. Теперь мы уверены, что определенно достигли JavaScript высот! Или еще нет?
И приходит async/await
Несколько месяцев спустя async/await выходит на сцену. Он собирался попасть в спецификацию ES7, затем идею отложили, но т.к. есть Babel, мы прыгнули в поезд. Мы узнали, что можем отметить функцию как асинхронную и что это ключевое слово позволит нам внутри функции «остановить» ее поток исполнения до тех пор, пока промис не решит, что наш код снова выглядит синхронным. Кроме того, функция async всегда будет возвращать промис, и мы можем использовать try/catch блоки для обработки ошибок.
Не слишком уверенные в пользе, мы даем нашему коду новый шанс и идем на окончательную реорганизацию.
async function asyncHandleRequest(req, res) {
try {
const { user } = req
await isUserValidAsync(user)
const [data, rate] = await Promise.all([getUserDataAsync(user), getRateAsync('service')])
const savedData = await updateUserDataAsync(user, updateData(data, rate))
res.send(savedData)
} catch (err) {
res.error('An error ocurred!')
}
}
И теперь код снова выглядит как старый обычный императивный синхронный код. Жизнь продолжилась как обычно, но что-то глубоко в вашей голове говорит нам что что-то здесь не так…
Функциональная парадигма программирования
Хотя функциональное программирование было вокруг нас в течение более чем 40 лет, похоже что совсем недавно парадигма стала набирать обороты. Похоже, что только в последнее время, мы стали понимать преимущества функционального подхода.
Мы начинаем обучение некоторым из его принципов. Изучаем новые слова, такие как функторы, монады, моноиды и вдруг наши dev-друзья начинают считать нас крутыми, потому что мы говорим эти странные слова довольно часто!
Мы продолжаем свое плавание в море функциональной парадигмы программирования и начинаем видеть ее реальную ценность. Эти сторонники функционального программирования не были просто сумасшедшими. Они были, возможно, правы!
Мы понимаем преимущества неизменяемости, чтобы не хранить и не мутировать состояние, чтобы создать сложную логику путем объединения простых функций, чтобы избежать управления циклами и чтобы вся магия была сделана самим интерпретатором языка, чтобы мы могли сосредоточиться на том, что действительно важно, чтобы избежать ветвления и обработки ошибок, просто комбинируя больше функций.
Но… постойте!
Мы видели все эти функциональные модели в прошлом. Мы помним, как мы использовали обещания и как соединяли функциональные преобразования один за другим, без необходимости управлять состоянием или ветвить наш код или управлять ошибками в императивном стиле. Мы уже использовали promise-монаду в прошлом со всеми сопутствующими преимуществами, но в то время, мы просто не знали это слово!
И мы вдруг понимаем, почему код на основе async/await смотрелся странно. Ведь мы писали обычный императивный код, как в 80-х годах, обрабатывали ошибки с try/catch, как в 90-х годах, управляли внутренним состоянием и переменными, делая асинхронные операции с помощью кода, который выглядит как синхронный, но который внезапно останавливается, а затем автоматически продолжает выполнение когда асинхронная операция будет завершена (когнитивный диссонанс?).
Последние мысли
Не поймите меня неправильно, async/await не является источником всего зла в мире. Я на самом деле научился любить его после нескольких месяцев использования. Если вы чувствуете себя комфортно, когда пишете императивный код, научиться использовать async/await для управления асинхронными операциями может быть хорошим ходом.
Но если вы любите промисы и хотите научиться применять все больше и больше функциональных принципов программирования, вы можете просто пропустить async/await, перестать думать императивно и перейти к новой-старой функциональной парадигме.
См. также
→ Еще одно мнение о том, что async/await не такая уж хорошая вещь.
Автор: zapolnoch