TL;DR:

  • Ключевое слово await принимает функцию, которая возвращает обещание, и возвращает разрешенное значение этого обещания.
  • Вы можете использовать ключевое слово await только в функциях, которые были объявлены с ключевым словом async.
  • Как async функции работают под капотом.
  • Вам не нужно использовать await в инструкции return функции async при возврате обещания.
  • Вы можете использовать Promise.all([…]) с await для одновременного выполнения нескольких обещаний.
  • Вы можете использовать цикл for/await для асинхронного цикла по обещаниям.

Вступление

ES2017 представил два ключевых слова, которые представили совершенно новую парадигму асинхронного программирования в JavaScript. async и await значительно упрощают использование обещаний и позволяют синхронно писать асинхронный код на основе обещаний. Несмотря на то, что async и await скрывают весь код, необходимый для простых промисов, по-прежнему очень важно знать, как работают промисы, и какой API они предоставляют, чтобы максимально использовать async/await.

Если вы не знаете, как работают обещания, я рекомендую вам прочитать мои предыдущие две статьи о том, что вам нужно знать о обещаниях. Часть 1 описывает, когда промисы выполняются в цикле событий, а Часть 2 описывает, как работают промисы, предоставляемый ими API и некоторые общие шаблоны.

С учетом сказанного, давайте разберемся, как работает async/await!

Ожидайте выражений

Ключевое слово await принимает функцию, которая возвращает обещание, и возвращает разрешенное значение этого обещания. Если разрешение обещания выполнено, то возвращается выполненное значение, в противном случае оно бросает причину отклонения. Давайте посмотрим на пример:

const users = await fetch('/users');

Предполагая, что fetch(…) возвращает Promise, выражение await fetch(…) будет ждать, пока fetch(…) не будет урегулировано. Если разрешение fetch(…) является выполненным, то users присваивается возвращаемое значение. Однако, если fetch(…) по какой-либо причине обнаруживает ошибку и отклоняется, выражение await fetch(…) выбрасывает причину отклонения.

Важно отметить, что выражение await не заставляет вашу программу останавливаться и ничего не делать до тех пор, пока указанное обещание не будет выполнено. Вы можете думать о await как о простом сокрытии .then(…, …) вызова, который используется для обработки разрешения Promises.

Асинхронные функции

Поскольку любой код, использующий await, является асинхронным, есть одно правило, которое вы должны обязательно соблюдать: вы можете использовать ключевое слово await только в функциях, которые были объявлены с ключевым словом async. Вот пример:

async getFoo() {
  const foo = await fetch('/api/v1/getFoo');
}

Обратите внимание, что когда вы объявляете функцию как async, это означает, что функция вернет обещание, даже если в теле функции нет кода, связанного с обещаниями. Учтите следующее:

async addFiftyTwo(number) {
  const plusFiftyTwo = number + 52;
  return plusFiftyTwo;
}

Под капотом

Когда вы посмотрите, как async функции работают под капотом, вы увидите, что это просто функции, которые возвращают обещания, как описано в этом посте. Давайте посмотрим на пример:

async function get52() {
  return 52;
}
// The following works exactly the same
function get52() {
  return new Promise((resolve, reject) => {
    resolve(52);
  });
}
// Which can also be written as 
function get52() {
  return Promise.resolve(52);
}

Вот почему все еще очень важно понимать концепции обещаний для эффективного использования async/await. async/await - это всего лишь синтаксический сахар вокруг обещаний, поэтому, если вы не знаете, как эффективно использовать обещания, вы, скорее всего, не сможете эффективно использовать и async/await. Давайте теперь рассмотрим некоторые общие шаблоны, используемые с async/await.

Общие шаблоны

Возвращение обещаний

Распространенная ошибка, которую делают новички, начиная с async/await, - когда использовать await, а когда не использовать. Учтите следующее:

async someFunc() {
  return await fetch('/api/v1/getFoo');
}
async someOtherFunc() {
  const foo = await someFunc();
}

Как упоминалось ранее, функция async возвращает обещание, которое разрешается до значения возвращенного из функции, а await ожидает разрешения обещания перед продолжением выполнения функции async. Если функция async возвращает обещание как значение return, вам не нужно использовать await после return ключевого слова. Посмотрим, как это будет выглядеть

async someFunc() {
  return fetch('/api/v1/getFoo');
}
async someOtherFunc() {
  const foo = await someFunc();
}

Как бы вы это прочитали? someFunc() возвращает обещание, которое разрешается в другое обещание, которое разрешается в ответ от вызова API. Итак, await в someOtherFucn() будет ждать разрешения самого внутреннего обещания и присвоить последнее значение переменной foo.

Ожидание нескольких обещаний

Предположим, вы написали getJSON функцию, которая выглядит примерно так

async function getJSON(url) {
    const response = await fetch(url);
    const body = await response.json();
    return body;
}

И теперь вы хотите получить некоторые данные JSON из внешнего API, поэтому вы делаете это

async function doSomething() {
  const users = await getJSON('/api/v1/users');
  const themes = await getJSON('/api/v1/themes');
}

Время, необходимое для выполнения этой функции, полностью зависит от того, когда разрешены два getJSON вызова. Это означает, что если первый вызов длится 5 секунд, второй вызов не начнется до тех пор, а если второй вызов также займет 5 секунд, то для завершения функции в целом потребуется 10 секунд. Это может быть ненужным, если два вызова не зависят друг от друга. Это очень распространенная ошибка, которую часто совершают новые разработчики при работе с async/await. Так как же это исправить? Promise API предоставляет статический метод all([…]), который мы можем использовать. Посмотрим, как это работает.

Примечание. Я обсуждаю Promise.all([…]) и другие методы, которые предоставляет Promise API, в моем сообщении здесь.

async function doSomething() {
  const [users, themes] = await Promise.all([
    getJSON('/api/v1/users'),
    getJSON('/api/v1/themes')
  ]);
}

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

Асинхронные итерации

Итераторы и асинхронные итераторы выходят за рамки этой статьи, поэтому мы расскажем только о том, как async/await используются для асинхронных итераций. Асинхронные итераторы похожи на обычные итераторы, но основаны на обещаниях и могут использоваться в новой форме for/of циклов, for/await.

Как и обычное выражение await, цикл for/await основан на обещаниях. Метод next() на асинхронном итераторе создает обещание, цикл for/await ожидает разрешения этого обещания, присваивает значение выполнения переменной цикла и запускает тело цикла. Затем он начинается заново, получая еще одно обещание от итератора и ожидая разрешения этого нового обещания. Давайте посмотрим на пример того, как это использовать. В следующем примере мы будем использовать обычный итератор (не асинхронный итератор), но применима та же концепция.

Предположим, у вас есть массив URL-адресов, которые вы хотите перебрать и получить массив обещаний:

const urls = [url1, url2, url3];
const promises = urls.map((url) => fetch(url));

Теперь мы можем использовать Promise.all ([…]) здесь и ждать, пока все обещания будут разрешены, но что, если нам нужны результаты, как только они вернутся? И что? Мы можем сделать что-то вроде этого:

for(const promise of promises) {
  const response = await promise;
  // do something with response here...
}

Это вполне приемлемо, ничего плохого в этом нет. Поскольку итератор возвращает обещания, мы также можем воспользоваться циклом for/await, как показано ниже:

for await (const response of promises) {
  //do something with response here...
}

Цикл for/await использует вызов await в цикл и делает наш код немного более компактным, но два примера делают одно и то же. Следует отметить, что оба примера будут работать только в том случае, если они находятся внутри функций, объявленных async: цикл for/await в этом смысле совпадает с регулярным await выражением.

Заключение

К настоящему времени вы должны хорошо понимать async/await и то, как они работают. Мы рассмотрели ключевые слова по отдельности, а также заглянули внутрь того, как работают async функции. После этого некоторые общие шаблоны, которые используются с async/await, и некоторые типичные ошибки, которые допускают новые разработчики, когда начинают работать с async/await.

Как всегда, дайте мне знать, каков ваш опыт работы с async / await. С какими проблемами вы столкнулись при использовании этого и других вопросов, по которым у вас могут возникнуть вопросы.

До следующего раза, ура!