Існує спеціальний синтаксис для більш зручної роботи з промісами, який називається “async/await”. Його напрочуд легко зрозуміти та використовувати.
Асинхронні функції
Почнемо з ключового слова async
. Його можна розмістити перед функцією, наприклад:
async
function
f
(
)
{
return
1
;
}
Слово async
перед функцією означає одну просту річ: функція завжди повертає проміс. Інші значення автоматично загортаються в успішно виконаний проміс.
Наприклад, ця функція повертає успішно виконаний проміс з результатом 1
; протестуймо:
async
function
f
(
)
{
return
1
;
}
f
(
)
.
then
(
alert)
;
// 1
…Ми могли б явно повернути проміс, результат буде таким самим:
async
function
f
(
)
{
return
Promise.
resolve
(
1
)
;
}
f
(
)
.
then
(
alert)
;
// 1
Отже, async
гарантує, що функція повертає проміс і обгортає в нього не-проміси. Досить просто, правда? Але це ще не все. Є ще одне ключове слово, await
, яке працює лише всередині async
-функцій, і воно досить круте.
Await
Синтаксис:
// працює лише всередині async-функцій
let
value =
await
promise;
Ключове слово await
змушує JavaScript чекати, поки проміс не виконається, та повертає його результат.
Ось приклад з промісом, який виконується за 1 секунду:
async
function
f
(
)
{
let
promise =
new
Promise
(
(
resolve,
reject
)
=>
{
setTimeout
(
(
)
=>
resolve
(
"готово!"
)
,
1000
)
}
)
;
let
result =
await
promise;
// чекатиме, поки проміс не виконається (*)
alert
(
result)
;
// "готово!"
}
f
(
)
;
Виконання функції “призупиняється” у рядку (*)
і відновлюється, коли проміс виконається, а результатом стає result
. Отже, код вище показує “готово!” через одну секунду.
Підкреслимо: await
буквально призупиняє виконання функції до тих пір, поки проміс не виконається, а потім відновлює її з результатом проміса. Це не вимагає жодних ресурсів ЦП, тому що рушій JavaScript може тим часом робити інші завдання: виконувати інші скрипти, обробляти події тощо.
Це просто більш елегантний синтаксис отримання результату проміса, ніж promise.then
. Зокрема, так це легше читати й писати.
await
у звичайних функціяхЯкщо ми спробуємо використати await
у не-асинхронній функції, виникне синтаксична помилка:
function
f
(
)
{
let
promise =
Promise.
resolve
(
1
)
;
let
result =
await
promise;
// Syntax error
}
Ми можемо отримати цю помилку, якщо забудемо поставити async
перед функцією. Як було сказано раніше, await
працює лише всередині async
-функцій.
Давайте візьмемо за приклад showAvatar()
з розділу Ланцюжок промісів і перепишемо його за допомогою async/await
:
- Нам потрібно замінити виклики
.then
наawait
. - Також ми повинні оголосити функцію як
async
, щоб вони працювали.
async
function
showAvatar
(
)
{
// зчитуємо наш JSON
let
response =
await
fetch
(
'/article/promise-chaining/user.json'
)
;
let
user =
await
response.
json
(
)
;
// зчитуємо користувача github
let
githubResponse =
await
fetch
(
`
https://api.github.com/users/
${
user.
name}
`
)
;
let
githubUser =
await
githubResponse.
json
(
)
;
// показуємо аватар
let
img =
document.
createElement
(
'img'
)
;
img.
src =
githubUser.
avatar_url;
img.
className =
"promise-avatar-example"
;
document.
body.
append
(
img)
;
// очікуємо 3 секунди
await
new
Promise
(
(
resolve,
reject
)
=>
setTimeout
(
resolve,
3000
)
)
;
img.
remove
(
)
;
return
githubUser;
}
showAvatar
(
)
;
Досить зрозуміло та легко читається, правда? Набагато краще, ніж раніше.
await
верхнього рівня в модуляхУ сучасних браузерах await
на верхньому рівні працює чудово, якщо ми знаходимося всередині модуля. Ми розглянемо модулі в статті Вступ до модулів.
Наприклад:
// ми припускаємо, що цей код виконується на верхньому рівні вкладеності всередині модуля
let
response =
await
fetch
(
'/article/promise-chaining/user.json'
)
;
let
user =
await
response.
json
(
)
;
console.
log
(
user)
;
Якщо ми не використовуємо модулі, або повинні підтримувати старіші браузери, існує універсальний рецепт: загорнути код в анонімну асинхронну функцію.
Ось так:
(
async
(
)
=>
{
let
response =
await
fetch
(
'/article/promise-chaining/user.json'
)
;
let
user =
await
response.
json
(
)
;
...
}
)
(
)
;
await
працює з “thenable”-об’єктамиЯк і promise.then
, await
дозволяє нам використовувати thenable-об’єкти (їх можна викликати методом then
). Ідея полягає в тому, що сторонній об’єкт може не бути промісом, але бути сумісним з промісом: якщо він підтримує .then
, цього достатньо, щоб використовувати його з await
.
Ось приклад класу Thenable
; нижче await
приймає його екземпляри:
class
Thenable
{
constructor
(
num
)
{
this
.
num =
num;
}
then
(
resolve,
reject
)
{
alert
(
resolve)
;
// виконається з this.num*2 через 1000мс
setTimeout
(
(
)
=>
resolve
(
this
.
num *
2
)
,
1000
)
;
// (*)
}
}
async
function
f
(
)
{
// чекатиме 1 секунду, після чого результат стане 2
let
result =
await
new
Thenable
(
1
)
;
alert
(
result)
;
}
f
(
)
;
Якщо await
отримує об’єкт не-проміс із .then
, він викликає цей метод, що надає як аргументи вбудовані функції resolve
та reject
(так само як це робиться для звичайного виконання Promise
). Потім await
чекає, поки не буде викликано один з них (у вищенаведеному прикладі це відбувається в рядку (*)
), а потім переходить до результату.
Щоб оголосити асинхронний метод класу, просто додайте перед ним async
:
class
Waiter
{
async
wait
(
)
{
return
await
Promise.
resolve
(
1
)
;
}
}
new
Waiter
(
)
.
wait
(
)
.
then
(
alert)
;
// 1 (це те ж саме, що й (result => alert(result)))
Сенс той самий: це гарантує, що повернуте значення буде промісом, і дозволяє використовувати await
.
Обробка помилок
Якщо проміс виконується нормально, то await promise
повертає результат. Але у випадку завершення з помилкою він генерує помилку, ніби в цьому рядку був оператор throw
.
Цей код:
async
function
f
(
)
{
await
Promise.
reject
(
new
Error
(
"Упс!"
)
)
;
}
…робить те ж саме, що й цей:
async
function
f
(
)
{
throw
new
Error
(
"Упс!"
)
;
}
У реальних ситуаціях може пройти деякий час, перш ніж проміс завершиться з помилкою. У цьому випадку буде затримка, перш ніж await
видасть помилку.
Ми можемо зловити цю помилку за допомогою try..catch
, так само як і звичайний throw
:
async
function
f
(
)
{
try
{
let
response =
await
fetch
(
'http://no-such-url'
)
;
}
catch
(
err)
{
alert
(
err)
;
// TypeError: failed to fetch
}
}
f
(
)
;
У разі помилки керування переходить до блоку catch
. Ми також можемо обгорнути таким чином кілька рядків:
async
function
f
(
)
{
try
{
let
response =
await
fetch
(
'/no-user-here'
)
;
let
user =
await
response.
json
(
)
;
}
catch
(
err)
{
// перехоплює помилки як у fetch, так і в response.json
alert
(
err)
;
}
}
f
(
)
;
Якщо у нас немає try..catch
, тоді проміс, згенерований викликом асинхронної функції f()
, завершиться з помилкою. Ми можемо додати .catch
для її обробки:
async
function
f
(
)
{
let
response =
await
fetch
(
'http://no-such-url'
)
;
}
// f() поверне проміс, що завершився з помилкою
f
(
)
.
catch
(
alert)
;
// TypeError: failed to fetch // (*)
Якщо ми забудемо додати .catch
, то отримаємо необроблену помилку проміса (можна переглянути в консолі). Ми можемо відловити такі помилки за допомогою глобального обробника події unhandledrejection
, як описано в розділі Проміси: обробка помилок.
async/await
та promise.then/catch
Коли ми використовуємо async/await
, нам рідко потрібен .then
, тому що await
обробляє очікування за нас. І ми можемо використовувати звичайний try..catch
замість .catch
. Зазвичай (але не завжди) це зручніше.
Але на верхньому рівні вкладеності коду, коли ми знаходимося за межами будь-якої функції async
, ми синтаксично не можемо використовувати await
, тому звичайна практика – додати .then/catch
для обробки кінцевого результату або помилки, що була повернута, як у рядку “(*)” у прикладі вище.
async/await
чудово працює з Promise.all
Коли нам потрібно дочекатися кількох промісів, ми можемо загорнути їх у Promise.all
, а потім додати await
:
// чекаємо масив результатів
let
results =
await
Promise.
all
(
[
fetch
(
url1)
,
fetch
(
url2)
,
...
]
)
;
У разі помилки вона передаватиметься як зазвичай: від невдалого проміса до Promise.all
, а потім стає винятком(exception), яку ми можемо зловити за допомогою try..catch
навколо виклику.
Підсумки
Ключове слово async
перед функцією має два ефекти:
- Змушує її завжди повертати проміс.
- Дозволяє використовувати в ній
await
.
Ключове слово await
перед промісом змушує JavaScript чекати, поки цей проміс не виконається, а потім:
- Якщо це помилка, генерується виняток(exception) – так само, ніби
throw error
було викликано саме в цьому місці. - В іншому випадку він повертає результат.
Разом вони забезпечують чудову структуру для написання асинхронного коду, який легко і читати, і писати.
За допомогою async/await
нам рідко потрібно писати promise.then/catch
, але ми все одно не повинні забувати, що вони засновані на промісах, тому що іноді (наприклад, на верхньому рівні вкладеності) нам доводиться використовувати ці методи. Також Promise.all
стає в нагоді, коли ми чекаємо на виконання багатьох завдань одночасно.
Коментарі
<code>
, для кількох рядків – обгорніть їх тегом<pre>
, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)