В предыдущей части становится понятно, что использовать функции обратного вызова (callback) для обработки асинхронного кода не очень-то эффективно. В ES6 была добавлена концепция обещаний (promises), которые будут рассмотрены далее.
Обещания (Promises)
Обещание (promise) представляет собой завершение асинхронной функции. Это объект, который может вернуть значение когда-нибудь в будущем. Он выполняет ту же миссию, что функция обратного вызова (callback), но с множеством дополнительных фич и более читаемым синтаксисом.
Создание обещания (Promise)
Promise инициализируется с помощью new Promise(fn)
, и ему следует передавать функцию. Функция, которая передается в обещание, имеет параметры для разрешения (resolve
) и отклонения (reject
). Функции resolve
и reject
обрабатывают, соответственно, успех и неудачу операции. Пример:
// Инициализация Promise
const promise = new Promise((resolve, reject) => {})
Если проверить инициализированный promise в этом состоянии с помощью консоли веб-браузера, можно обнаружить, что оно имеет статус pending
и значение undefined
:
__proto__: Promise
[[PromiseStatus]]: "pending"
[[PromiseValue]]: undefined
Пока для promise ничего не настроено, оно будет оставаться в состоянии pending
. Первое, что уже можно сделать, чтобы проверить promise – это выполнить promise, разрешив его с каким-нибудь значением:
const promise = new Promise((resolve, reject) => {
resolve('Мы сделали это!')
})
Теперь, после проверки, можно обнаружить, что статус promise изменился на fulfilled
, а у value
значению, которое было передано в функцию resolve
:
__proto__: Promise
[[PromiseStatus]]: "fulfilled"
[[PromiseValue]]: "Мы сделали это!"
Как указано в начале этого раздела, promise – это объект, который может возвращать значение. После успешного выполнения содержимое value
меняется с undefined
на заполненное данными. У обещания бывает три возможных состояния: отложено (pending
), выполнено (fulfilled
) и отклонено (rejected
).
Pending
– исходное состояние до разрешения или отклоненияFulfilled
– успешная операция, обещание разрешеноRejected
– неудачная операция, обещание отклонено
После выполнения или отклонения обещание считается выполненным. Теперь, когда у есть представление о том, как создаются promise, посмотрим, как их можно использовать.
Использование обещания (Promise)
Promise в последнем примере выполнено со значением, поэтому можно получить доступ к этому значению. У обещаний есть метод then
, который выполнится после того, как в коде оно доберётся до resolve
.then
вернёт значение promise в качестве параметра. Вот пример, как это можно сделать:
promise.then(response => {
console.log(response)
})
Значением [[PromiseValue]]
из примера выше было “Мы сделали это!”. Это значение будет передано анонимной функции в качестве response
:
Мы сделали это!
До сих пор этот пример не включал асинхронный веб-API и только объяснял, как создавать, обрабатывать и использовать встроенное обещание (promise). Используя setTimeout
, можно имитировать асинхронный запрос и посмотреть, как это работает:
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('Разрешение асинхронного запроса!'), 2000)
})
// Результат
promise.then(response => {
console.log(response)
})
Использование синтаксиса then
гарантирует, что response
попадёт в консоль только через 2000 миллисекунд, когда операция setTimeout
будет завершена. Все это делается без вложенных обратных вызовов (callback).
Теперь, через две секунды, разрешится значение promise и then
напишет в консоль
Разрешение асинхронного запроса!
Обещания также могут быть связаны в цепочку для передачи данных нескольких асинхронных операций. Каждый then
в этой цепочке может изменять принимаемое значение и передавать его дальше в следующий then
.
// promise-цепочка
promise
.then(firstResponse => {
// возвращает новое значение для следующего then
return firstResponse + ' И цепочка!'
})
.then(secondResponse => {
console.log(secondResponse)
})
Выполненное во втором then
обещание напишет в консоль:/p>
Разрешение асинхронного запроса! И цепочка!
Выстраивание then
в цепочку, делает внешний вид кода для promise более читаемым, чем у вложенных обратных вызовов, его будет проще поддерживать и проверять.
Обработка ошибок
До сего времени рассматривались примеры promise только с успешным разрешением resolve
, которое переводит обещание в состояние fullfilled
(выполнено). Но часто с асинхронным запросом необходимо обрабатывать ошибки – если API не работает, отправляется неверный или неавторизованный запрос. Promise должен обрабатывать оба случая.
В этом примере в функцию getUsers
передается флаг, а она возвращает promise.
function getUsers(onSuccess) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// имитация разрешения и отклонения в асинхронном API
}, 1000)
})
}
Теперь дополняем код так, чтобы, когда в onSuccess
передано true
, таймаут выполнялся с некоторыми данными. Если false
, promise будет отклонен с ошибкой.
function getUsers(onSuccess) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// имитация разрешения и отклонения в асинхронном API
if (onSuccess) {
resolve([
{ id: 1, name: 'Пётр' },
{ id: 2, name: 'Фёдор' },
{ id: 3, name: 'Егор' },
])
} else {
reject('Ошибка получения данных!')
}
}, 1000)
})
}
Для успешного результата возвращается JavaScript-объект, который имитирует список пользователей.
Для того, чтобы обработать ошибку, следует использовать метод catch
. Он возвращает ошибку, которую можно обработать в передаваемом ему в качестве параметра обратном вызове.
Для примера, вызываем функцию getUser
с параметром onSuccess
, установленным в false
, используем метод then
для обработки успеха и метод catch
для ошибки:
// чтобы имитировать ошибку,
// надо запустить функции getUsers с флагом false
getUsers(false)
.then(response => {
console.log(response)
})
.catch(error => {
console.error(error)
})
Поскольку promise был отклонен и имитирована ошибка, then
будет пропущен, а ошибка будет обработана в catch
:
Ошибка получения данных!
Если флаг, передаваемый в getUser
переключить в true
, promise будет разрешено успешно, выполнится код внутри then
, а catch
игнорироваться.
// чтобы имитировать успех,
// надо запустить функции getUsers с флагом true
getUsers(true)
.then(response => {
console.log(response)
})
.catch(error => {
console.error(error)
})
B будет имитироваться асинхронное получение списка пользователей:
(3) [{…}, {…}, {…}]
0: {id: 1, name: "Пётр"}
1: {id: 2, name: "Фёдор"}
3: {id: 3, name: "Егор"}
Подведём итог:
then()
- Обрабатывает
resolve
. Возвращает promise и асинхронно вызывает функциюonFulfilled
. catch()
- Обрабатывает
reject
. Возвращает promise и асинхронно вызывает функциюonRejected
. finally()
- Вызывается, когда обещание выполнено. Возвращает promise и вызывает асинхронную функцию
onFinally
.
Promise могут сбивать с толку как новичков, так и опытных программистов, которые никогда раньше не работали в асинхронной среде. Однако, как уже упоминалось, гораздо чаще приходится использовать promise, чем создавать их. Обычно веб-API браузера или сторонняя библиотека предоставляют promise, и остается только использовать его.
В последнем разделе о promise в этом руководстве будет приведен типичный пример использования веб-API, который возвращает обещания: Fetch API.
Использование Fetch API с promise
Одним из наиболее полезных и часто используемых веб-API, возвращающих promise, является Fetch API, который позволяет выполнять асинхронные сетевые запросы. fetch
– это процесс, состоящий из двух частей, поэтому он требует цепочки then
. В этом примере демонстрируется использование API GitHub для получения данных пользователя, а также обработка любой потенциальной ошибки:
// Получить пользователя из API GitHub
fetch('https://api.github.com/users/octocat')
.then(response => {
return response.json()
})
.then(data => {
console.log(data)
})
.catch(error => {
console.error(error)
})
Асинхронный запрос методом fetch
отправляется на URL https://api.github.com/users/octocat
и ожидает получения ответа. Первый then
передает ответ анонимной функции, которая отформатирует ответ в виде JSON, затем передает этот JSON второму then
, который отправляет данные в консоль. Оператор catch
записывает любую ошибку в консоль. Выполнение этого кода возвращает следующее:
login: "octocat",
id: 583231,
avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4"
blog: "https://github.blog"
company: "@github"
followers: 3203
...
Это данные, запрошенные с https://api.github.com/users/octocat
, представленные в формате JSON.
В этом разделе руководства показано, что promise включают в себя множество улучшений для работы с асинхронным кодом. Хотя использование then
для обработки асинхронных действий легче, чем пирамида обратных вызовов (callback), некоторые разработчики по-прежнему предпочитают синхронный формат написания асинхронного кода. Чтобы удовлетворить эту потребность и упростить работу с обещаниями, ECMAScript 2016 (ES7) предлагает функцию async
и ключевое слово await
. В следующей части рассматривается использование async
/await
.