До появления async/await при разработке асинхронных механизмов программ использовались коллбэки и промисы. Автор материала, перевод которого мы публикуем сегодня, предлагает сначала вспомнить о том, как писать код по-старому, а потом, на реальном примере, изучить применение async/await.
Коллбэки
Вот как выглядит пример кода, в котором используются коллбэки (функции обратного вызова):
setTimeout(() => {
console.log('This runs after 1000 milliseconds.');
}, 1000);
При использовании вложенных функций обратного вызова возникает проблема, называемая «адом коллбэков». Вот упрощённый пример кода, иллюстрирующий эту проблему:
asyncCallOne(() => {
asyncCallTwo(() => {
asyncCallThree(() => {
asyncCallFour(() => {
asyncCallFive(() => {
//выполнение неких действий
})
})
})
})
})
Если в коде присутствуют структуры, состоящие из вложенных друг в друга коллбэков, такой код может оказаться трудным для понимания, его будет сложно поддерживать.
Промисы
Вот пример кода, в котором используются промисы (Promise-объекты):
const promiseFunction = new Promise((resolve, reject) => {
const add = (a, b) => a + b;
resolve(add(2, 2));
});
promiseFunction.then((response) => {
console.log(response);
}).catch((error) => {
console.log(error);
});
Функция
promiseFunction()
из этого примера возвращает объект Promise
, который представляет собой некое действие, выполняемое функцией. Вызов метода resolve()
указывает промису на то, что его работа успешно завершена. В подобных конструкциях используется и метод промисов .then()
— с его помощью, после успешного разрешения промиса, можно выполнить некий коллбэк. Метод .catch()
вызывается в тех случаях, если в ходе работы промиса что-то пошло не так.Асинхронные функции
Функции, объявленные с использованием ключевого слова
async
(асинхронные функции), дают нам возможность писать аккуратный и не перегруженный служебными конструкциями код, позволяющий получить тот же результат, который мы получали с использованием промисов. Надо отметить, что ключевое слово async
— это, в сущности, лишь «синтаксический сахар» для промисов.Асинхронные функции создают, пользуясь при объявлении функции ключевым словом
async
. Выглядит это так:const asyncFunction = async () => {
// Код
}
Выполнение асинхронной функции можно приостановить с использованием ключевого слова
await
. Его можно использовать только в асинхронных функциях. Оно позволяет возвратить результат работы асинхронной функции, который будет доступен после того, как такая функция завершит выполнение некоей задачи.Сравним работу асинхронной функции и промиса, которые возвращают строку:
// Асинхронная функция
const asyncGreeting = async () => 'Greetings';
// Промис
const promiseGreeting = () => new Promise(((resolve) => {
resolve('Greetings');
}));
asyncGreeting().then(result => console.log(result));
promiseGreeting().then(result => console.log(result));
Несложно заметить, что использование ключевого слова
async
позволяет писать асинхронный код, который выглядит как синхронный. С таким кодом гораздо легче работать.Теперь, когда мы рассмотрели базовые вещи, перейдём к нашему примеру.
Конвертер валют
?Предварительная подготовка
Здесь мы создадим простое, но познавательное с точки зрения изучения конструкции async/await приложение. Оно представляет собой конвертер валют, который использует реальные данные, получаемые из соответствующих API. Программа принимает сумму в некоей валюте, код этой валюты, а также код валюты, в которую мы хотим конвертировать эту сумму. После этого программа выводит результат, предварительно загрузив актуальные данные по курсам валют. Программа также выводит список стран, в которых можно потратить деньги в той валюте, в которую осуществляется перевод заданной суммы.
В частности, здесь мы собираемся пользоваться данными из двух асинхронных источников информации:
- Сервис currencylayer.com. На этом сайте нужно будет завести бесплатную учётную запись и получить ключ для доступа к API (API Access Key). Отсюда мы будем брать данные, необходимые для конверсии суммы из одной валюты в другую.
- Сервис restcountries.eu. Им можно пользоваться без регистрации. Отсюда мы загрузим данные о том, где можно пользоваться валютой, в которую мы конвертировали заданную сумму денег.
Создадим новую директорию и выполним в ней команду
npm init
. Когда программа задаст нам вопрос об имени создаваемого пакета — введём currency-converter
. На остальные вопросы программы можно не отвечать, нажимая в ответ Enter
. После этого установим в нашем проекте пакет Axios, выполнив в его директории команду npm install axios --save
. Создадим новый файл с именем currency-converter.js
.Приступим к написанию кода программы, подключив в этом файле Axios:
const axios = require('axios');
В нашем проекте будет три асинхронные функции. Первая будет загружать данные о валютах. Вторая будет загружать данные о странах. Третья будет эти данные собирать, представлять в удобном для пользователя виде и выводить на экран.
?Первая функция — асинхронная загрузка данных о валютах
Создадим асинхронную функцию
getExchangeRate()
, которая будет принимать два аргумента: fromCurrency
и toCurrency
:const getExchangeRate = async (fromCurrency, toCurrency) => {}
В этой функции нам надо загрузить данные. Благодаря использованию конструкции async/await можно записывать полученные данные непосредственно в некую переменную или константу. Перед написанием кода этой функции не забудьте зарегистрироваться на сайте и получить ключ доступа к API. Для загрузки данных нам понадобится следующая конструкция:
const response = await axios.get('http://www.apilayer.net/api/live?access_key=[ваш код доступа к API]');
После получения ответа системы нужные нам данные можно будет обнаружить в объекте
response
по адресу response.data.quotes
. Вот как выглядит фрагмент объекта с интересующими нас данными (он виден в программе как response.data
):{
"success":true,
"terms":"https:\/\/currencylayer.com\/terms",
"privacy":"https:\/\/currencylayer.com\/privacy",
"timestamp":1547891348,
"source":"USD",
"quotes":{
"USDAED":3.673042,
"USDAFN":75.350404,
"USDALL":109.203989,
...
"USDZWL":322.355011
}
}
Поместим объект с курсами валют в константу
rate
:const rate = response.data.quotes;
Код базовой валюты можно найти по адресу
response.data.source
. Запишем код базовой валюты в константу baseCurrency
:const baseCurrency = response.data.source;
Так как по умолчанию данные, возвращаемые этим API, представляют собой курс валюты по отношению к американскому доллару (USD), создадим константу
usd
, в которую запишем результат деления 1 на курс валюты, в которой задана сумма:const usd = 1 / rate[`${baseCurrency}${fromCurrency}`];
Обратите внимание на то, как формируется ключ, по которому мы получаем значение курса. В объекте, получаемом из API, фрагмент которого приведён выше, ключи представляют собой строки, начинающиеся с
USD
и заканчивающиеся кодом соответствующей валюты. Так как подразумевается, что наша программа принимает строковые коды валют, мы формируем ключ, конкатенируя строку, содержащую код базовой валюты, и то, что передано функции в параметре fromCurrency
.Теперь, для того, чтобы получить курс обмена валюты
fromCurrency
на валюту toCurrence
, мы умножаем константу usd
на курс для toCurrency
. Выглядит это так:const exchangeRate = usd * rate[`${baseCurrency}${toCurrency}`];
В итоге то, что попадёт в
exchangeRate
, мы из функции возвращаем. Вот как выглядит её полный код:const getExchangeRate = async (fromCurrency, toCurrency) => {
try {
const response = await axios.get('http://www.apilayer.net/api/live?access_key=[ваш код доступа к API]');
const rate = response.data.quotes;
const baseCurrency = response.data.source;
const usd = 1 / rate[`${baseCurrency}${fromCurrency}`];
const exchangeRate = usd * rate[`${baseCurrency}${toCurrency}`];
return exchangeRate;
} catch (error) {
throw new Error(`Unable to get currency ${fromCurrency} and ${toCurrency}`);
}
};
Обратите внимание на то, что для обработки ошибок, которые могут возникнуть в ходе выполнения запроса, используется обычная конструкция try/catch.
?Вторая функция — асинхронная загрузка данных о странах
Наша вторая функция,
getCountries()
, асинхронно загружающая информацию о странах, в которых можно пользоваться валютой, в которую мы преобразуем заданную в другой валюте сумму, будет принимать аргумент currencyCode
:const getCountries = async (currencyCode) => {}
Для загрузки данных мы пользуемся следующей командой:
const response = await axios.get(`https://restcountries.eu/rest/v2/currency/${currencyCode}`);
Если, например, мы используем в запросе код
HRK
(хорватская куна), то в ответ нам придёт JSON-код, фрагмент которого показан ниже:[
{
"name":"Croatia",
...
}
]
Он представляет собой массив объектов с информацией о странах. В свойствах
name
этих объектов содержится название страны. Обратиться к этому массиву можно с помощью конструкции response.data
. Применим метод массивов map()
для извлечения названий стран из полученных данных и возвратим из функции getCountries()
эти данные, которые будут представлять собой массив названий стран:return response.data.map(country => country.name);
Вот как выглядит полный код функции
getCountries()
:const getCountries = async (currencyCode) => {
try {
const response = await axios.get(`https://restcountries.eu/rest/v2/currency/${currencyCode}`);
return response.data.map(country => country.name);
} catch (error) {
throw new Error(`Unable to get countries that use ${currencyCode}`);
}
};
?Третья функция — сбор и вывод данных
Наша третья асинхронная функция,
convertCurrency ()
, будет принимать аргументы fromCurrency
, toCurrency
и amount
— коды валют и сумму.const convertCurrency = async (fromCurrency, toCurrency, amount) => {}
В ней мы сначала получаем обменный курс:
const exchangeRate = await getExchangeRate(fromCurrency, toCurrency);
Потом загружаем список стран:
const countries = await getCountries(toCurrency);
Далее — выполняем конверсию:
const convertedAmount = (amount * exchangeRate).toFixed(2);
А после того, как все необходимые данные собраны — возвращаем строку, которую увидит пользователь программы:
return `${amount} ${fromCurrency} is worth ${convertedAmount} ${toCurrency}. You can spend these in the following countries: ${countries}`;
Вот полный код функции:
const convertCurrency = async (fromCurrency, toCurrency, amount) => {
const exchangeRate = await getExchangeRate(fromCurrency, toCurrency);
const countries = await getCountries(toCurrency);
const convertedAmount = (amount * exchangeRate).toFixed(2);
return `${amount} ${fromCurrency} is worth ${convertedAmount} ${toCurrency}. You can spend these in the following countries: ${countries}`;
};
Обратите внимание на то, что в этой функции нет блока try/catch, так как она работает лишь с результатами, предоставленными ей двумя ранее описанными функциями.
?Запуск программы
Мы подготовили три функции, две из которых загружают данные из различных сервисов, а одна собирает эти данные и готовит к выводу. Теперь нам осталось лишь вызвать эту функцию, передав ей всё необходимое. Мы не будем реализовывать здесь механизмы, позволяющие вызывать нашу программу из командной строки с передачей ей кодов валют и сумм, хотя вы, при желании, вполне можете это сделать. Мы просто вызовем функцию
convertCurrency()
, передав ей необходимые данные:convertCurrency('EUR', 'HRK', 20)
.then((message) => {
console.log(message);
}).catch((error) => {
console.log(error.message);
});
Здесь мы хотим выяснить — на сколько хорватских кун можно обменять 20 евро и попутно узнать о том, в каких странах можно эти деньги потратить.
Вызовем программу, введя в терминале следующую команду:
node currency-converter.js
В ответ мы получим примерно следующее.
Результат работы программы
Вот, на всякий случай, полный код нашего проекта.
const axios = require('axios');
const getExchangeRate = async (fromCurrency, toCurrency) => {
try {
const response = await axios.get('http://www.apilayer.net/api/live?access_key=[ваш код доступа к API]');
const rate = response.data.quotes;
const baseCurrency = response.data.source;
const usd = 1 / rate[`${baseCurrency}${fromCurrency}`];
const exchangeRate = usd * rate[`${baseCurrency}${toCurrency}`];
return exchangeRate;
} catch (error) {
throw new Error(`Unable to get currency ${fromCurrency} and ${toCurrency}`);
}
};
const getCountries = async (currencyCode) => {
try {
const response = await axios.get(`https://restcountries.eu/rest/v2/currency/${currencyCode}`);
return response.data.map(country => country.name);
} catch (error) {
throw new Error(`Unable to get countries that use ${currencyCode}`);
}
};
const convertCurrency = async (fromCurrency, toCurrency, amount) => {
const exchangeRate = await getExchangeRate(fromCurrency, toCurrency);
const countries = await getCountries(toCurrency);
const convertedAmount = (amount * exchangeRate).toFixed(2);
return `${amount} ${fromCurrency} is worth ${convertedAmount} ${toCurrency}. You can spend these in the following countries: ${countries}`;
};
convertCurrency('EUR', 'HRK', 20)
.then((message) => {
console.log(message);
}).catch((error) => {
console.log(error.message);
});
Итоги
Надеемся, этот пример использования конструкции async/await в реальных условиях помог тем, кто раньше этой конструкции не понимал, в ней разобраться.
Уважаемые читатели! Если вы используете на практике конструкцию async/await — просим поделиться впечатлениями о ней.
Комментарии (20)
Kurienko
23.01.2019 15:53-5я не понимаю зачем для даной задачи использовать асинхрон? зачем все так сложно? можно сделать функцию что приймет валюту и по формуле даст результат при клике на кнопку Результат или создать собитие addEventListener, (что есть вроде тоже асинхроним собитием) и при вводе суми в input слушать его собите изменения значения… без всяких там ожиданий первой под фунции, для виполнения второй под функции
напишите реальний пример, боевой из прода, но простой для чайника, плз)karl93rus
23.01.2019 16:49+4Так вот тебе реальный пример из прода в статье. Получение данных из апи. При чем здесь ивентЛисенер? Пока промис не отработает, тебе инфа не придёт. Надо дождаться. Можно ждать колбэками, можно промисами, можно асинками. Просто асинки удобны. Грубо говоря, пока не выполнится первый await, второй будет ждать. Второму надо получить инфу из первого. Без нее он будет работать с undefined. Если я правильно понял пример.
lleo_aha
Типичная ошибка использования await — в функции convertCurrency есть два запроса (getExchangeRates и getCountries) которые друг друга не блокируют, но второй зачем то ждёт выполнения первого
karl93rus
То есть функция, которая внутри себя работает с, например, двумя асинхронными, может просто вызываться без всяких async/await? А сами асинхронные функции и так отработают по очереди?
akdes
Вы уверены? как мне кажется, если не ждать одну или другую, может случится казус, что вторая функция, которую ждали, отработает при том что первая нет. А тут мы уже отдачу делаем…
можно переписать, и выдавать информацию по поступлению, но если цель отдать инфу вместе… посему может есть ошибка в дизайне, но не в использовании await
CoolCmd
Promise.all()
akdes
Статья, заметьте, про async/await, объясняющая функционал наглядным примером.
Удержание равновесия на мотоцикле можно показать на велосипеде, если цель в постановке примера…
CoolCmd
я статью не читал. обычно при описании await первым делом говорят, что это — те же обещания, только в более удобной форме. соответственно, для понимания работы await нужно знать как работают обещания и какие для них есть полезные функции, та же Promise.all().
akdes
достаточно прочитать наименование статьи… ;)
Я не спорю, и Вы и lleo_aha в корне правы.
Однако, это как мне кажеться, как писать в каждой статье «Как написать xyz на PHP», что питон лучше. В корне может и так оно и есть, но это не отменяет права на использование PHP для решения задачи. Так и await/async, как мне кажется более или менее был показан конструкт сей функции автором. Реализация примера для этих целей, второстепенна, если она исчерпывающая.
vadim5june
если было бы важно выполнять функции паралельно то по моему без Promise.all не обойтись.
можно написать
async function parallel(m){return await Promise.all(m)};
и потом обращаемся
const m=await parallel([getExchangeRate(fromCurrency, toCurrency),getCountries(toCurrency)]);
получим массив из двух ответов
akdes
можете объяснить, чем Ваш пример лучше авторского?
по факту идёт тоже самое, только написано по другому. Или я не прав?
vadim5june
У автора выполняются последовательно
getCountries запустится когда придет ответ от getExchangeRate
у меня они одновременно запускаются
akdes
понятно, я «плоско» думал только о результате, но Ваш пример по теории быстрее отработает. спасибо
evkochurov
А какой смысл в этой функции? Скрыть из кода упоминание Promise? Но асинхронная функция всегда возвращает обещание, поэтому было бы достаточно написать так:
Эффект тот же, но работать будет быстрее, так как не создает совершенно лишнее по смыслу обещание-обертку, которое разрешается после разрешения обещания Promise.all(). Проверял только на NodeJS v8.11.3, возможно, когда-нибудь оптимизатор и такие вещи научится упрощать.
Но на мой взгляд, гораздо лучше вызывать Promise.all() напрямую, без оберток. В любом сколько-нибудь реальном применении async/await все равно придется разбираться с обещаниями, так что скрывать их из кода нет смысла.
vadim5june
Да там внутри функции await лишнее.
Спасибо.
Смысл избавится в основном коде от промисов.
Пример без этой функции ниже тут два раза написали
holodoz
Здесь удержание равновесия демонстрируется на трёхколёсном велосипеде, что может дать неверное представление о паттернах асинхронного программирования, ну, или удержания равновесия. Небольшое усложнение в виде Promise.all немного расширит кругозор и покажет, как делать правильнее.
akdes
promise.all, как мне кажется, делает то тоже самое… Если нет, можете объяснить что происходит в авторском примере под компотом и что в Вашем. Спасибо
UPDATE: мне ответили уже Выше, спасибо. с промисами работа будет паралельной и поидеи отработает быстрее…
buldezir
а как в данном случае правильно сделать так чтобы они друг друга не блокировали, но дальнейший код выполнялся только после завершения обоих запросов?
mayorovp
Использовать Promise.all
someone_unimportant
async/await
— это те же самые промисы, просто с альтернативным синтаксисом, поэтому вместоawait
для каждой отдельной функции можно использоватьawait Promise.all
.Upd: Прошу прощения за похожий ответ. На момент публикации моего комментария, других ответов не было, но мой комментарий проходил премодерацию.