JavaScript: Понимание асинхронных вызовов

Источник: «Understanding Async Await»
При написании кода для web, в конечном итоге вам требуется выполнить некий процесс, который может занять несколько минут. JavaScript не может работать в многозадачном режиме, поэтому нам нужен способ обработки этих длительных процессов.

Async/Await — это способ обработки этого типа временной последовательности. Это особенно удобно, когда вам нужно сделать какой-то сетевой запрос, а затем работать с полученными данными. Давайте разберёмся!

Обещаешь? Обещаю

Async/Await — разновидность Promise (Обещание). Промисы в JavaScript — это объекты, которые могут иметь несколько состояний (так же как и обещания в реальной жизни ☺️). Промисы делают так, потому что иногда то, что мы просим, не доступно сразу, и нам нужна возможность определить, в каком состоянии они находятся.

Представьте, что кто-то просит вас пообещать что-то сделать для него, например, помочь переехать. Есть начальное состояние, где они попросили. Но вы не выполнили данное им обещание, пока не появились и не помогли им переехать. Если вы отмените свои планы, вы отвергните обещание.

Точно так же три возможных состояния Промиса в JavaScript:

Вот пример промиса в этих состояниях:

Здесь выполненное состояние. Мы сохраняем Промис в 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

See the Pen

Если мы выберем отклонённое состояние (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!

See the Pen

Одно ждёт другое

Одним из распространённых способов использования 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()

Вот анимированное объяснение процесса:

See the Pen

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 хорошо поддерживается, мы не стали проходить все эти этапы. Они всё ещё остаются хорошей базой, особенно если вам нужно поддерживать старый код. Вот некоторые из моих любимых ресурсов:

Дополнительные материалы

Предыдущая Статья

Как преобразовать Node.js Buffer в String

Следующая Статья

Топ-5 генераторов статических сайтов в 2022