--- title: Making asynchronous programming easier with async and await slug: Learn/JavaScript/Asynchronous/Async_await translation_of: Learn/JavaScript/Asynchronous/Async_await ---
В ECMAScript версии 2017 появились async functions и ключевое слово await
(ECMAScript Next support in Mozilla). По существу, такие функции есть синтаксический сахар над Promises и Generator functions (ts39). С их помощью легче писать/читать асинхронный код, ведь они позволяют использовать привычный синхронный стиль написания. В этой статье мы на базовом уровне разберемся в их устройстве.
Примечания: | Чтобы лучше понять материал, желательно перед чтением ознакомиться с основами JavaScript, асинхронными операциями вообще и объектами Promises. |
---|---|
Цель материала: | Научить писать современный асинхронный код с использованием Promises и async functions. |
Ключевое слово 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 перед promise-based функцией, чтобы остановить поток выполнения и дождаться результата ее выполнения (результат Promise). В то же время, остальной код нашего приложения не блокируется и продолжает работать.
Вы можете использовать await
перед любой функцией, что возвращает Promise, включая Browser API функции.
Небольшой пример:
async function hello() { return greeting = await Promise.resolve("Hello"); }; hello().then(alert);
Конечно, на практике код выше бесполезен, но в учебных целях он иллюстрирует синтаксис асинхронных функций. Теперь давайте перейдем к реальным примерам.
Давайте посмотрим на пример из предыдущей статьи:
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:
Как вы помните, асинхронные функции построены поверх 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 позволяет вам писать код в синхронном стиле. Ключевое слово 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.
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);
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.
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.
{{PreviousMenuNext("Learn/JavaScript/Asynchronous/Promises", "Learn/JavaScript/Asynchronous/Choosing_the_right_approach", "Learn/JavaScript/Asynchronous")}}