Making asynchronous programming easier with async and await

Этот перевод не завершён. Пожалуйста, помогите перевести эту статью с английского

В ECMAScript версии 2017 появились async functions и ключевое слово await (ECMAScript Next support in Mozilla). По существу, такие функции есть синтаксический сахар над Promises и Generator functions (ts39). С их помощью легче писать/читать асинхронный код, ведь они позволяют использовать привычный синхронный стиль написания. В этой статье мы на базовом уровне разберемся в их устройстве.

Примечания: Чтобы лучше понять материал, желательно перед чтением ознакомиться с основами JavaScript, асинхронными операциями вообще и объектами Promises.
Цель материала: Научить писать современный асинхронный код с использованием Promises и async functions.

Основы async/await

Ключевое слово async

Ключевое слово async позволяет сделать из обычной функции (function declaration или function expression) асинхронную функцию (async function). Такая функция делает две вещи:
- Оборачивает возвращаемое значение в Promise
- Позволяет использовать ключевое слово await (см. дальше)

Попробуйте выполнить в консоли браузера следующий код:

function hello() { return "Hello" };
hello();

Функция возвращает "Hello" — ничего необычного, верно ?

Но что если мы сделаем ее асинхронной ? Проверим:

async function hello() { return "Hello" };
hello();

Как было сказано ранее, вызов асинхронной функции возвращает объект Promise.

Вот пример с async function expression:

let hello = async function() { return "Hello" };
hello();

Также можно использовать стрелочные функции:

let hello = async () => { return "Hello" };

Все они в общем случае делают одно и то же.

Чтобы получить значение, которое возвращает Promise, мы как обычно можем использовать метод .then():

hello().then((value) => console.log(value))

или еще короче

hello().then(console.log)

Итак, ключевое слово async, превращает обычную функцию в асинхронную и результат вызова функции оборачивает в Promise. Также асинхронная функция позволяет использовать в своем теле ключевое слово await, о котором далее.

Ключевое слово await

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

Вы можете использовать await перед любой функцией, что возвращает Promise, включая Browser API функции.

Небольшой пример:

async function hello() {
  return greeting = await Promise.resolve("Hello");
};

hello().then(alert);

Конечно, на практике код выше бесполезен, но в учебных целях он иллюстрирует синтаксис асинхронных функций. Теперь давайте перейдем к реальным примерам.

Переписываем Promises с ипользованием async/await

Давайте посмотрим на пример из предыдущей статьи:

fetch('coffee.jpg')
.then(response => {
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    return response.blob();
  }
})
.then(myBlob => {
  let objectURL = URL.createObjectURL(myBlob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
})
.catch(e => {
  console.log('There has been a problem with your fetch operation: ' + e.message);
});

К этому моменту вы должны понимать как работают Promises, чтобы понять все остальное. Давайте перепишем код используя async/await и оценим разницу.

async function myFetch() {
  let response = await fetch('coffee.jpg');

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    let myBlob = await response.blob();

    let objectURL = URL.createObjectURL(myBlob);
    let image = document.createElement('img');
    image.src = objectURL;
    document.body.appendChild(image);
  }
}

myFetch()
.catch(e => {
  console.log('There has been a problem with your fetch operation: ' + e.message);
});

Согласитесь, что код стал короче и понятнее — больше никаких блоков .then() по всему скрипту!

Так как ключевое слово async заставляет функцию вернуть Promise, мы можем использовать гибридный подход:

async function myFetch() {
  let response = await fetch('coffee.jpg');
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    return await response.blob();
  }
}

myFetch().then((blob) => {
  let objectURL = URL.createObjectURL(blob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}).catch(e => console.log(e));

Можете попрактиковаться самостоятельно, или запустить наш live example (а также source code).

Минуточку, а как это все работает ?

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

Внутри myFetch() находится код, который слегка напоминает версию на Promise, но есть важные отличия. Вместо того, чтобы писать цепочку блоков .then() мы просто использует ключевое слово await перед вызовом promise-based функции и присваиваем результат в переменную. Ключеовое слово await говорит JavaScript runtime приостановить код в этой строке, не блокируя остальной код скприта за пределами асинхронной функции. Когда вызов promise-based функции будет готов вернуть результат, выполнение продолжится с этой строки дальше.

Пример:

let response = await fetch('coffee.jpg');

Значение Promise, которое вернет fetch() будет присвоено переменной response только тогда, когда оно будет доступно - парсер делает паузу на данной строке дожидаясь этого момента. Как только значение доступно, парсер переходит к следующей строке, в которой создается объект Blob из результата Promise. В этой строке, кстати, также используется await, потому что метод .blob() также возвращет Promise. Когда результат готов, мы возвращаем его наружу из myFetch().

Обратите внимание, когда мы вызываем myFetch(), она возвращает Promise, поэтому мы можем вызвать .then() на результате, чтобы отобразить его на экране.

К этому моменту вы наверное думаете "Это реально круто!", и вы правы - чем меньше блоков .then(), тем легче читать код.

Добавляем обработку ошибок


Чтобы обработать ошибки у нас есть несколько вариантов

Мы можем использовать синхронную try...catch структуру с async/await. Вот измененная версия первого примера выше:

async function myFetch() {
  try {
    let response = await fetch('coffee.jpg');

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    } else {
      let myBlob = await response.blob();
      let objectURL = URL.createObjectURL(myBlob);
      let image = document.createElement('img');
      image.src = objectURL;
      document.body.appendChild(image);
    }
  } catch(e) {
    console.log(e);
  }
}

myFetch();

В блок catch() {} передается объект ошибки, который мы назвали e; мы можем вывести его в консоль, чтобы посмотреть детали: где и почему возникла ошибка.

Если вы хотите использовать гибридный подходы (пример выше), лучше использовать блок .catch() после блока .then() вот так:

async function myFetch() {
  let response = await fetch('coffee.jpg');
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    return await response.blob();
  }
}

myFetch().then((blob) => {
  let objectURL = URL.createObjectURL(blob);
  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
})
.catch((e) =>
  console.log(e)
);

Так лучше, потому что блок .catch() словит ошибки как из асинхронной функции, так и из Promise. Если бы мы использовали блок try/catch, мы бы не словили ошибку, которая произошла в самой myFetch() функции.

Вы можете посмотреть оба примера на GitHub:

Await и Promise.all()

Как вы помните, асинхронные функции построены поверх promises, поэтому они совместимы со всеми возможностями последних. Мы легко можем подождать выполнение Promise.all(), присвоить результат в переменную и все это сделать используя синхронный стиль. Опять, вернемся к an example we saw in our previous article. Откройте пример в соседней вкладке, чтобы лучше понять разницу.

Версия с async/await (смотрите live demo и source code), сейчас выглядит так:

async function fetchAndDecode(url, type) {
  let response = await fetch(url);

  let content;

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  } else {
    if(type === 'blob') {
      content = await response.blob();
    } else if(type === 'text') {
      content = await response.text();
    }

    return content;
  }

}

async function displayContent() {
  let coffee = fetchAndDecode('coffee.jpg', 'blob');
  let tea = fetchAndDecode('tea.jpg', 'blob');
  let description = fetchAndDecode('description.txt', 'text');

  let values = await Promise.all([coffee, tea, description]);

  let objectURL1 = URL.createObjectURL(values[0]);
  let objectURL2 = URL.createObjectURL(values[1]);
  let descText = values[2];

  let image1 = document.createElement('img');
  let image2 = document.createElement('img');
  image1.src = objectURL1;
  image2.src = objectURL2;
  document.body.appendChild(image1);
  document.body.appendChild(image2);

  let para = document.createElement('p');
  para.textContent = descText;
  document.body.appendChild(para);
}

displayContent()
.catch((e) =>
  console.log(e)
);

Вы видите, что мы легко изменили fetchAndDecode() функцию в асинхронный вариант. Взгляните на строку с Promise.all():

let values = await Promise.all([coffee, tea, description]);

С помощью await мы ждем массив результатов всех трех Promises и присваиваем его в переменную values. Это асинхронный код, но он написан в синхронном стиле, за счет чего он гораздо читабельнее.

Мы должны обернуть весь код в синхронную функцию, displayContent(), и мы не сильно сэкономили на количестве кода, но мы извлекли код блока .then(), за счет чего наш код стал гораздо чище.

Для обработки ошибок мы добавили блок .catch() для функции displayContent(); Это позволило нам отловить ошибки в обоих функциях.

Заметка: Мы также можем использовать синхронный блок finally внутри асинхронной функции, вместо асинхронного .finally(), чтобы получить информацию о результате нашей операции — смотрите в действии в нашем live example (смотрите source code).

Недостатки async/await

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

Async/await позволяет вам писать код в синхронном стиле. Ключевое слово await блокирует приостанавливает выполнение ptomise-based функции до того момента, пока promise примет статуc fulfilled. Это не блокирует код за пределами вашей асинхронной функции, тем не менее важно помнить, что внутри асинхронной функции поток выполнения блокируется.

Ваш код может стать медленнее за счет большого количества awaited promises, которые идут один за другим. Каждый await должен дождаться выполнения предыдущего, тогда как на самом деле мы хотим, чтобы наши Promises выполнялись одновременно, как если бы мы не использовали async/await.

Есть подход, который позволяет обойти эту проблему - сохранить все выполняющиеся Promises в переменные, а уже после этого дожидаться (awaiting) их результата. Давайте посмотрим на несколько примеров.

We've got two examples available — slow-async-await.html (see source code) and fast-async-await.html (see source code). Both of them start off with a custom promise function that fakes an async process with a setTimeout() call:

function timeoutPromise(interval) {
  return new Promise((resolve, reject) => {
    setTimeout(function(){
      resolve("done");
    }, interval);
  });
};

Then each one includes a timeTest() async function that awaits three timeoutPromise() calls:

async function timeTest() {
  ...
}

Each one ends by recording a start time, seeing how long the timeTest() promise takes to fulfill, then recording an end time and reporting how long the operation took in total:

let startTime = Date.now();
timeTest().then(() => {
  let finishTime = Date.now();
  let timeTaken = finishTime - startTime;
  alert("Time taken in milliseconds: " + timeTaken);
})

It is the timeTest() function that differs in each case.

In the slow-async-await.html example, timeTest() looks like this:

async function timeTest() {
  await timeoutPromise(3000);
  await timeoutPromise(3000);
  await timeoutPromise(3000);
}

Here we simply await all three timeoutPromise() calls directly, making each one alert for 3 seconds. Each subsequent one is forced to wait until the last one finished — if you run the first example, you'll see the alert box reporting a total run time of around 9 seconds.

In the fast-async-await.html example, timeTest() looks like this:

async function timeTest() {
  const timeoutPromise1 = timeoutPromise(3000);
  const timeoutPromise2 = timeoutPromise(3000);
  const timeoutPromise3 = timeoutPromise(3000);

  await timeoutPromise1;
  await timeoutPromise2;
  await timeoutPromise3;
}

Here we store the three Promise objects in variables, which has the effect of setting off their associated processes all running simultaneously.

Next, we await their results — because the promises all started processing at essentially the same time, the promises will all fulfill at the same time; when you run the second example, you'll see the alert box reporting a total run time of just over 3 seconds!

You'll have to test your code carefully, and bear this in mind if performance starts to suffer.

Another minor inconvenience is that you have to wrap your awaited promises inside an async function.

Async/await class methods

As a final note before we move on, you can even add async in front of class/object methods to make them return promises, and await promises inside them. Take a look at the ES class code we saw in our object-oriented JavaScript article, and then look at our modified version with an async method:

class Person {
  constructor(first, last, age, gender, interests) {
    this.name = {
      first,
      last
    };
    this.age = age;
    this.gender = gender;
    this.interests = interests;
  }

  async greeting() {
    return await Promise.resolve(`Hi! I'm ${this.name.first}`);
  };

  farewell() {
    console.log(`${this.name.first} has left the building. Bye for now!`);
  };
}

let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);

The first class method could now be used something like this:

han.greeting().then(console.log);

Browser support

One consideration when deciding whether to use async/await is support for older browsers. They are available in modern versions of most browsers, the same as promises; the main support problems come with Internet Explorer and Opera Mini.

If you want to use async/await but are concerned about older browser support, you could consider using the BabelJS library — this allows you to write your applications using the latest JavaScript and let Babel figure out what changes if any are needed for your user’s browsers. On encountering a browser that does not support async/await, Babel's polyfill can automatically provide fallbacks that work in older browsers.

Conclusion

And there you have it — async/await provide a nice, simplified way to write async code that is simpler to read and maintain. Even with browser support being more limited than other async code mechanisms at the time of writing, it is well worth learning and considering for use, both for now and in the future.

In this module