Конструкция async/await представляет собой сравнительно новый подход к написанию асинхронного кода в JavaScript. Она основана на промисах и, в результате, не блокирует главный поток. Новшество этой конструкции заключается в том, что благодаря ей асинхронный код становится похожим на синхронный и ведёт себя подобным образом. Это открывает перед программистом замечательные возможности.

image

До появления 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. Программа принимает сумму в некоей валюте, код этой валюты, а также код валюты, в которую мы хотим конвертировать эту сумму. После этого программа выводит результат, предварительно загрузив актуальные данные по курсам валют. Программа также выводит список стран, в которых можно потратить деньги в той валюте, в которую осуществляется перевод заданной суммы.

В частности, здесь мы собираемся пользоваться данными из двух асинхронных источников информации:

  1. Сервис currencylayer.com. На этом сайте нужно будет завести бесплатную учётную запись и получить ключ для доступа к API (API Access Key). Отсюда мы будем брать данные, необходимые для конверсии суммы из одной валюты в другую.
  2. Сервис 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)


  1. lleo_aha
    23.01.2019 12:06
    +10

    Типичная ошибка использования await — в функции convertCurrency есть два запроса (getExchangeRates и getCountries) которые друг друга не блокируют, но второй зачем то ждёт выполнения первого


    1. karl93rus
      23.01.2019 14:46

      То есть функция, которая внутри себя работает с, например, двумя асинхронными, может просто вызываться без всяких async/await? А сами асинхронные функции и так отработают по очереди?


    1. akdes
      23.01.2019 14:54

      Вы уверены? как мне кажется, если не ждать одну или другую, может случится казус, что вторая функция, которую ждали, отработает при том что первая нет. А тут мы уже отдачу делаем…
      можно переписать, и выдавать информацию по поступлению, но если цель отдать инфу вместе… посему может есть ошибка в дизайне, но не в использовании await


      1. CoolCmd
        23.01.2019 15:36
        +2

        Promise.all()


        1. akdes
          23.01.2019 15:59
          -2

          Статья, заметьте, про async/await, объясняющая функционал наглядным примером.
          Удержание равновесия на мотоцикле можно показать на велосипеде, если цель в постановке примера…


          1. CoolCmd
            23.01.2019 16:38
            +3

            я статью не читал. обычно при описании await первым делом говорят, что это — те же обещания, только в более удобной форме. соответственно, для понимания работы await нужно знать как работают обещания и какие для них есть полезные функции, та же Promise.all().


            1. akdes
              23.01.2019 16:45
              -2

              достаточно прочитать наименование статьи… ;)
              Я не спорю, и Вы и lleo_aha в корне правы.
              Однако, это как мне кажеться, как писать в каждой статье «Как написать xyz на PHP», что питон лучше. В корне может и так оно и есть, но это не отменяет права на использование PHP для решения задачи. Так и await/async, как мне кажется более или менее был показан конструкт сей функции автором. Реализация примера для этих целей, второстепенна, если она исчерпывающая.


          1. vadim5june
            23.01.2019 16:47
            +1

            если было бы важно выполнять функции паралельно то по моему без Promise.all не обойтись.
            можно написать
            async function parallel(m){return await Promise.all(m)};
            и потом обращаемся
            const m=await parallel([getExchangeRate(fromCurrency, toCurrency),getCountries(toCurrency)]);
            получим массив из двух ответов


            1. akdes
              23.01.2019 16:55
              -2

              можете объяснить, чем Ваш пример лучше авторского?
              по факту идёт тоже самое, только написано по другому. Или я не прав?


              1. vadim5june
                23.01.2019 17:01
                +2

                У автора выполняются последовательно
                getCountries запустится когда придет ответ от getExchangeRate
                у меня они одновременно запускаются


                1. akdes
                  23.01.2019 17:06

                  понятно, я «плоско» думал только о результате, но Ваш пример по теории быстрее отработает. спасибо


            1. evkochurov
              23.01.2019 21:30

              async function parallel(m){return await Promise.all(m)};

              А какой смысл в этой функции? Скрыть из кода упоминание Promise? Но асинхронная функция всегда возвращает обещание, поэтому было бы достаточно написать так:
              function parallel(m){return Promise.all(m)};

              Эффект тот же, но работать будет быстрее, так как не создает совершенно лишнее по смыслу обещание-обертку, которое разрешается после разрешения обещания Promise.all(). Проверял только на NodeJS v8.11.3, возможно, когда-нибудь оптимизатор и такие вещи научится упрощать.

              Но на мой взгляд, гораздо лучше вызывать Promise.all() напрямую, без оберток. В любом сколько-нибудь реальном применении async/await все равно придется разбираться с обещаниями, так что скрывать их из кода нет смысла.


              1. vadim5june
                23.01.2019 21:49

                Да там внутри функции await лишнее.
                Спасибо.
                Смысл избавится в основном коде от промисов.
                Пример без этой функции ниже тут два раза написали


          1. holodoz
            23.01.2019 16:47

            Здесь удержание равновесия демонстрируется на трёхколёсном велосипеде, что может дать неверное представление о паттернах асинхронного программирования, ну, или удержания равновесия. Небольшое усложнение в виде Promise.all немного расширит кругозор и покажет, как делать правильнее.


            1. akdes
              23.01.2019 16:58
              -2

              promise.all, как мне кажется, делает то тоже самое… Если нет, можете объяснить что происходит в авторском примере под компотом и что в Вашем. Спасибо
              UPDATE: мне ответили уже Выше, спасибо. с промисами работа будет паралельной и поидеи отработает быстрее…


    1. buldezir
      23.01.2019 16:54

      а как в данном случае правильно сделать так чтобы они друг друга не блокировали, но дальнейший код выполнялся только после завершения обоих запросов?


      1. mayorovp
        23.01.2019 17:44
        +1

        Использовать Promise.all


        const convertCurrency = async (fromCurrency, toCurrency, amount) => {
            const [exchangeRate, countries] = await Promise.all([
                getExchangeRate(fromCurrency, toCurrency),
                getCountries(toCurrency),
            ]);
            const convertedAmount = (amount * exchangeRate).toFixed(2);
        
            return `${amount} ${fromCurrency} is worth ${convertedAmount} ${toCurrency}. You can spend these in the following countries: ${countries}`;
        };


      1. someone_unimportant
        23.01.2019 18:00

        async/await — это те же самые промисы, просто с альтернативным синтаксисом, поэтому вместо await для каждой отдельной функции можно использовать await Promise.all.


        const convertCurrency = async (fromCurrency, toCurrency, amount) => {
            const [ exchangeRate, countries ] = await Promise.all([
                getExchangeRate(fromCurrency, toCurrency),
                getCountries(toCurrency)
            ]);
            const convertedAmount = (amount * exchangeRate).toFixed(2);
        
            return `${amount} ${fromCurrency} is worth ${convertedAmount} ${toCurrency}. You can spend these in the following countries: ${countries}`;
        };

        Upd: Прошу прощения за похожий ответ. На момент публикации моего комментария, других ответов не было, но мой комментарий проходил премодерацию.


  1. Kurienko
    23.01.2019 15:53
    -5

    я не понимаю зачем для даной задачи использовать асинхрон? зачем все так сложно? можно сделать функцию что приймет валюту и по формуле даст результат при клике на кнопку Результат или создать собитие addEventListener, (что есть вроде тоже асинхроним собитием) и при вводе суми в input слушать его собите изменения значения… без всяких там ожиданий первой под фунции, для виполнения второй под функции

    напишите реальний пример, боевой из прода, но простой для чайника, плз)


    1. karl93rus
      23.01.2019 16:49
      +4

      Так вот тебе реальный пример из прода в статье. Получение данных из апи. При чем здесь ивентЛисенер? Пока промис не отработает, тебе инфа не придёт. Надо дождаться. Можно ждать колбэками, можно промисами, можно асинками. Просто асинки удобны. Грубо говоря, пока не выполнится первый await, второй будет ждать. Второму надо получить инфу из первого. Без нее он будет работать с undefined. Если я правильно понял пример.