JavaScript: Понимание асинхронных вызовов
Async/Await — это способ обработки этого типа временной последовательности. Это особенно удобно, когда вам нужно сделать какой-то сетевой запрос, а затем работать с полученными данными. Давайте разберёмся!
Обещаешь? Обещаю
Async/Await — разновидность Promise (Обещание). Промисы в JavaScript — это объекты, которые могут иметь несколько состояний (так же как и обещания в реальной жизни ☺️). Промисы делают так, потому что иногда то, что мы просим, не доступно сразу, и нам нужна возможность определить, в каком состоянии они находятся.
Представьте, что кто-то просит вас пообещать что-то сделать для него, например, помочь переехать. Есть начальное состояние, где они попросили. Но вы не выполнили данное им обещание, пока не появились и не помогли им переехать. Если вы отмените свои планы, вы отвергните обещание.
Точно так же три возможных состояния Промиса в JavaScript:
pending: Ожидание — когда вы впервые вызываете Промис и неизвестно, что он вернёт.
fulfilled: Выполнено — означает, что операция завершена успешно.
rejected: Отклонено — операция не удалась.
Вот пример промиса в этих состояниях:
Здесь выполненное состояние. Мы сохраняем Промис в getSomeTacos
, передавая параметры resolve
и reject
. Мы сообщаем промису, что он разрешён resolve()
, и это выводит два сообщения в консоль.
const getSomeTacos = new Promise((resolve, reject) => {
console.log("Initial state: Excuse me can I have some tacos");
resolve();
})
.then(() => {
console.log("Order some tacos");
})
.then(() => {
console.log("Here are your tacos");
})
.catch(err => {
console.error("Nope! No tacos for you.");
});
> Initial state: Excuse me can I have some tacos
> Order some tacos
> Here are your tacos
Если мы выберем отклонённое состояние (Rejected), мы выполним туже функцию, но на этот раз отклоним её. В консоль будет выведено исходное состояние (initial state) и сообщение об ошибке.
const getSomeTacos = new Promise((resolve, reject) => {
console.log("Initial state: Excuse me can I have some tacos");
reject();
})
.then(() => {
console.log("Order some tacos");
})
.then(() => {
console.log("Here are your tacos");
})
.catch(err => {
console.error("Nope! No tacos for you.");
});
> Initial state: Excuse me can I have some tacos
> Nope! No tacos for you.
И когда мы выбираем состояние ожидания (Pending State), мы выводим в консоль то, что мы сохранили, getSomeTacos
. Это выводит состояние ожидания, потому что это состояние в котором находится промис когда мы его зарегистрировали!
console.log(getSomeTacos)
> Initial state: Excuse me can I have some 🌮s
> Promise {<pending>}
> Order some 🌮s
> Here are your 🌮s
Что тогда?
Но вот что меня сначала смутило. Чтобы получить значение из промиса, вы должны использовать .then()
или что-то что возвращает разрешение вашего промиса. Если подумать, то это имеет смысл. Вам нужно зафиксировать, каким оно будет в конечном итоге — вместо того, чем оно является изначально. Изначально оно будет находиться в состоянии pending
- ожидание. Вот почему мы получили Promise {<pending>}
, когда вывели в консоль состояние промиса выше. На тот момент, ещё ничего не выполнилось.
Async/Await на самом деле — синтаксический сахар поверх промисов, которые вы только что видели. Вот небольшой пример, кака я мог бы использовать его вместе с промисом отложив несколько выполнений.
async function tacos() {
return await Promise.resolve("Now and then I get to eat delicious tacos!")
};
tacos().then(console.log)
Или более подробный пример:
// Это функция у которой мы хотим отложить выполнение. Это промис.
const addOne = (x) => {
return new Promise(resolve => {
setTimeout(() => {
console.log(`I added one! Now it's ${x + 1}.`)
resolve()
}, 2000);
})
}
// мы немедленно выводим первое сообщение в консоль,
// затем будет запущен промис addOne, что займёт 2 секунды
// затем будет выведено финальное сообщение
async function addAsync() {
console.log('I have 10')
await addOne(10)
console.log(`Now I'm done!`)
}
addAsync()
> I have 10
> I added one! Now it's 11.
> Now I'm done!
Одно ждёт другое
Одним из распространённых способов использования Async/Await — использование для цепочки из нескольких асинхронных вызовов. Здесь мы получаем данные JSON, которые будем использовать для передачи в следующий вызов, чтобы выяснить, какого типа данные мы получим от второго API. В нашем случае мы хотим получить доступ к программистским шуткам, но сначала нам нужно узнать из другого API, какой тип цитаты мы хотим.
Первый JSON файл выглядит так — мы хотим, чтобы тип цитаты был случайным:
{
"type": "random"
}
Второй API вернёт что-то похожее на это, учитывая тот параметр запроса random
, мы только что получили:
{
"_id":"5a933f6f8e7b510004cba4c2",
"en":"For all its power, the computer is a harsh taskmaster. Its programs must be correct, and what we wish to say must be said accurately in every detail.",
"author":"Alan Perlis",
"id":"5a933f6f8e7b510004cba4c2"
}
Мы вызываем асинхронную функцию, затем ждём получение первого JSON файла, до запроса данных из API. Как только это произойдёт, мы сможем сделать что-нибудь с этим ответом, например добавить его на нашу страницу.
async function getQuote() {
// получаем тип цитаты из первого запроса, всё остальное ждёт завершения этого запроса
let quoteTypeResponse = await fetch("https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/quotes.json")
let quoteType = await quoteTypeResponse.json()
// используем полученные данные из первого запроса в вызове второго API, всё остальное ждёт завершения этого запроса
let quoteResponse = await fetch("https://programming-quotes-api.herokuapp.com/quotes/" + quoteType.type)
let quote = await quoteResponse.json()
// Заканчиваем
console.log("done")
}
Мы можем упростить эту функцию используя литералы шаблонов и стрелочные функции:
async function getQuote() {
// получаем тип цитаты из первого запроса, всё остальное ждёт завершения этого запроса
let quoteType = await fetch(`quotes.json`).then(res => res.json())
// используем полученные данные из первого запроса в вызове второго API, всё остальное ждёт завершения этого запроса
let quote = await fetch(`programming-quotes.com/${quoteType.type}`).then(res => res.json())
// Заканчиваем
console.log('done')
}
getQuote()
Вот анимированное объяснение процесса:
Try, Catch, Finally
Со временем мы захотим добавить в этот процесс отслеживание ошибок. Для этого у нас есть удобные блоки try
, catch
и finally
.
try {
// Пытаемся выполнить код
}
catch(error) {
// Обрабатываем ошибки в коде
}
finally {
// Выполняется в любом случае
}
Реструктурируем приведённый выше код, для использования этого синтаксиса и отлавливания любых ошибок.
async function getQuote() {
try {
// получаем тип цитаты из первого запроса, всё остальное ждёт завершения этого запроса
let quoteType = await fetch(`quotes.json`).then(res => res.json())
// используем полученные данные из первого запроса в вызове второго API, всё остальное ждёт завершения этого запроса
let quote = await fetch(`programming-quotes.com/${quoteType.type}`).then(res => res.json())
// Заканчиваем
console.log('done')
}
catch(error) {
console.warn(`We have an error here: ${error}`)
}
}
getQuote()
Мы не использовали finally
, так это не всегда нужно. Этот блок исполняется независимо от того была ошибка или нет. Рассматривайте возможность использования блока finally
каждый раз, когда у вас что-то дублируется в try
и catch
. Обычно, использую его для отчистки кода. Я написала об этом статью, если вам интересно узнать больше.
Со временем вам может понадобиться более сложная обработка ошибок, такая как способ отмены асинхронной функции. К сожалению, нет способа сделать это изначально. Но, к счастью, Кайл Симпсон создал библиотеку CAF, которая может помочь.
Дополнительные материалы
Обычно объяснения Async/Await начинается с коллбэков/обратных вызовов, затем промисов, и эти объяснения используются для перехода к Async/Await. Поскольку в наши дни Async/Await хорошо поддерживается, мы не стали проходить все эти этапы. Они всё ещё остаются хорошей базой, особенно если вам нужно поддерживать старый код. Вот некоторые из моих любимых ресурсов:
Async JavaScript: From Callbacks, to Promises, to Async/Await (Tyler McGinnis)
Asynchronous JavaScript with async/await (Marius Schulz)
Mastering Async JavaScript (James K. Nelson)