В этом уроке вы узнаете, как писать асинхронный код, используя ключевые слова async
/ await
в JavaScript.
Обратите внимание: чтобы понять, как работает async
/ await
, нужно знать, как работают промисы.
В прошлом для работы с асинхронными операциями часто использовались функции обратного вызова. Однако, когда вы вкладываете много функций обратного вызова, код будет сложнее поддерживать. И в конечном итоге вы сталкиваетесь с печально известной проблемой, известной как ад обратного вызова.
Предположим, что вам нужно выполнить три асинхронных операции в следующей последовательности:
- Выберите пользователя из базы данных.
- Получить услуги пользователя из API.
- Рассчитать стоимость услуги на основе услуг с сервера.
Следующие функции иллюстрируют три задачи. Обратите внимание, что мы используем функцию setTimeout()
для имитации асинхронной операции:
function getUser(userId, callback) { console.log('Get user from the database.'); setTimeout(() => { callback({ userId: userId, username: 'john' }); }, 1000); } function getServices(user, callback) { console.log(`Get services of ${user.username} from the API.`); setTimeout(() => { callback(['Email', 'VPN', 'CDN']); }, 2 * 1000); } function getServiceCost(services, callback) { console.log(`Calculate service costs of ${services}.`); setTimeout(() => { callback(services.length * 100); }, 3 * 1000); }
Ниже показаны вложенные функции обратного вызова:
getUser(100, (user) => { getServices(user, (services) => { getServiceCost(services, (cost) => { console.log(`The service cost is ${cost}`); }); }); });
Выход:
Get user from the database. Get services of john from the API. Calculate service costs of Email,VPN,CDN. The service cost is 300
Чтобы избежать этой проблемы ада обратных вызовов, ES6 представила промисы, которые позволяют вам писать асинхронный код более управляемыми способами.
Во-первых, вам нужно вернуть Promise
в каждой функции:
function getUser(userId) { return new Promise((resolve, reject) => { console.log('Get user from the database.'); setTimeout(() => { resolve({ userId: userId, username: 'john' }); }, 1000); }) } function getServices(user) { return new Promise((resolve, reject) => { console.log(`Get services of ${user.username} from the API.`); setTimeout(() => { resolve(['Email', 'VPN', 'CDN']); }, 2 * 1000); }); } function getServiceCost(services) { return new Promise((resolve, reject) => { console.log(`Calculate service costs of ${services}.`); setTimeout(() => { resolve(services.length * 100); }, 3 * 1000); }); }
Затем вы связываете обещания:
getUser(100) .then(getServices) .then(getServiceCost) .then(console.log);
ES2017 представил ключевые слова async
/ await
, которые строятся поверх промисов, позволяя вам писать асинхронный код, который больше похож на синхронный код и более удобочитаем. С технической точки зрения, async
/ await
— это синтаксический сахар для промисов.
Если функция возвращает обещание, вы можете поместить ключевое слово await
перед вызовом функции, например:
let result = await f();
Ожидание будет await
, пока Promise
, возвращенный из f()
, будет установлен. Ключевое слово await
можно использовать только внутри async
функций.
Ниже определяется async
функция, которая последовательно вызывает три асинхронных операции:
async function showServiceCost() { let user = await getUser(100); let services = await getServices(user); let cost = await getServiceCost(services); console.log(`The service cost is ${cost}`); } showServiceCost();
Как видите, асинхронный код теперь выглядит как синхронный.
Давайте углубимся в ключевые слова async/await.
Ключевое слово async
Ключевое слово async
позволяет определить функцию, которая обрабатывает асинхронные операции.
Чтобы определить функцию, вы помещаете ключевое слово async
перед ключевым словом function следующим образом:
async function sayHi() { return 'Hi'; }
Асинхронные функции выполняются асинхронно через цикл обработки событий. Он всегда возвращает Promise
.
В этом примере, поскольку sayHi()
возвращает Promise
, вы можете использовать его следующим образом:
sayHi().then(console.log);
Вы также можете явно вернуть Promise
из функции sayHi()
, как показано в следующем коде:
async function sayHi() { return Promise.resolve('Hi'); }
Эффект тот же.
Помимо обычных функций, вы можете использовать ключевое слово async
в выражениях функций:
let sayHi = async function () { return 'Hi'; }
функции стрелки:
let sayHi = async() => 'Hi';
Методы занятий:
class Greeter { async sayHi() { return 'Hi'; } }
Ключевое слово await
Вы используете ключевое слово await
, чтобы дождаться, пока Promise
не установится либо в разрешенном, либо в отклоненном состоянии. И вы можете использовать ключевое слово await
только внутри async
функции:
async function display() { let result = await sayHi(); console.log(result); }
В этом примере ключевое слово await
указывает движку JavaScript дождаться завершения функции sayHi()
перед отображением сообщения.
Обратите внимание, что если вы используете оператор await
вне async
функции, вы получите сообщение об ошибке.
Обработка ошибок
Если обещание разрешается, await promise
возвращает результат. Однако, когда обещание отклоняется, await promise
выдаст ошибку, как если бы было выражение throw
.
Следующий код:
async function getUser(userId) { await Promise.reject(new Error('Invalid User Id')); }
То же самое, что и это:
async function getUser(userId) { throw new Error('Invalid User Id'); }
В реальном сценарии обещание выдаст ошибку через некоторое время.
Вы можете найти ошибку, используя оператор try...catch
, так же, как и обычный оператор throw
:
async function getUser(userId) { try { const user = await Promise.reject(new Error('Invalid User Id')); } catch(error) { console.log(error); } }
Можно найти ошибки, вызванные одним или несколькими await promise
:
async function showServiceCost() { try { let user = await getUser(100); let services = await getServices(user); let cost = await getServiceCost(services); console.log(`The service cost is ${cost}`); } catch(error) { console.log(error); } }