Кратко
СкопированоПромис (Promise) — специальный объект JavaScript, который используется для написания и обработки асинхронного кода.
Асинхронные функции возвращают объект Promise
в качестве значения. Внутри промиса хранится результат вычисления, которое может быть уже выполнено или выполнится в будущем.
Промис может находиться в одном из трёх состояний:
- pending — стартовое состояние, операция стартовала;
- fulfilled — получен результат;
- rejected — ошибка.
Поменять состояние можно только один раз: перейти из pending
либо в fulfilled
, либо в rejected
:
У промиса есть методы then
и catch
, которые позволяют использовать результат вычисления внутри промиса.
Как пишется
СкопированоПромис создаётся с помощью конструктора.
В конструктор передаётся функция-исполнитель асинхронной операции (англ. executor). Она вызывается сразу после создания промиса. Задача этой функции — выполнить асинхронную операцию и перевести состояние промиса в fulfilled
(успех) или rejected
(ошибка).
Изменить состояние промиса можно, вызвав колбэки, переданные аргументами в функцию:
const promise = new Promise(function (resolve, reject) { const data = getData() // делаем асинхронную операцию: запрос в БД, API, etc. resolve(data) // переводим промис в состояние fulfilled. Результатом выполнения будет объект data})const errorPromise = new Promise(function (resolve, reject) { reject(new Error('ошибка')) // переводим промис в состояние rejected. Результатом выполнения будет объект Error})
const promise = new Promise(function (resolve, reject) { const data = getData() // делаем асинхронную операцию: запрос в БД, API, etc. resolve(data) // переводим промис в состояние fulfilled. Результатом выполнения будет объект data }) const errorPromise = new Promise(function (resolve, reject) { reject(new Error('ошибка')) // переводим промис в состояние rejected. Результатом выполнения будет объект Error })
- первый параметр (в примере кода назван
resolve
) — колбэк для перевода промиса в состояниеfulfilled
, при его вызове аргументом передаётся результат операции; - второй параметр (в примере кода назван
reject
) — колбэк для перевода промиса в состояниеrejected
, при его вызове аргументом передаётся информация об ошибке.
Как понять
СкопированоПромис решает задачу выполнения кода, который зависит от результата асинхронной операции.
Промис устроен таким образом, что рычаги управления его состоянием остаются у асинхронной функции. После создания, промис находится в состоянии ожидания pending
. Когда асинхронная операция завершается, функция переводит промис в состояние успеха fulfilled
или ошибки rejected
.
С помощью методов then
, catch
и finally
мы можем реагировать на изменение состояния промиса и использовать результат его выполнения.
Пример ниже показывает состояние промиса, который создаётся при нажатии на кнопку купить. Промис случайным образом завершается успехом или ошибкой:
Методы
СкопированоВ работе мы чаще используем промисы, чем создаём. Использовать промис — значит выполнять код при изменении состояния промиса.
Существует три метода, которые позволяют работать с результатом выполнения вычисления внутри промиса:
then
( ) catch
( ) finally
( )
then
Метод then
используют, чтобы выполнить код после успешного выполнения асинхронной операции.
Например, мы запросили у сервера список фильмов и хотим отобразить их на экране, когда сервер получит результат. В этом случае:
- асинхронная операция — запрос данных у сервера;
- код, который мы хотим выполнить после её завершения, — отрисовка списка.
Метод then
принимает в качестве аргумента две функции-колбэка. Если промис в состоянии fulfilled
то выполнится первая функция. Если в состоянии rejected
— вторая. Хорошей практикой считается не использовать второй аргумент метода then
и обрабатывать ошибки при помощи метода catch
.
fetch(`https://swapi.dev/api/films/${id}/`).then(function (movies) { renderList(movies)})
fetch(`https://swapi.dev/api/films/${id}/`).then(function (movies) { renderList(movies) })
В коде выше, асинхронная функция fetch
возвращает промис, к которому применяется метод then
. При его выполнении в переменной movies
будет ответ сервера.
catch
Метод catch
используют, чтобы выполнить код в случае ошибки при выполнении асинхронной операции.
Например, мы запросили у сервера список фильмов и хотим показать экран обрыва соединения, если произошла ошибка. В этом случае:
- асинхронная операция — запрос данных у сервера;
- код, который мы хотим выполнить при ошибке — экран обрыва соединения.
Метод catch
принимает в качестве аргумента функцию-колбэк, которая выполняется сразу после того, как промис поменял состояние на rejected
. Параметр колбэка содержит экземпляр ошибки:
fetch(`https://swapi.dev/api/films/${id}/`).catch(function (error) { renderErrorMessage(error)})
fetch(`https://swapi.dev/api/films/${id}/`).catch(function (error) { renderErrorMessage(error) })
В коде выше, асинхронная функция fetch
возвращает промис, к которому применяется метод catch
. При его выполнении в переменной error
будет экземпляр ошибки.
finally
Метод finally
используют, чтобы выполнить код при завершении асинхронной операции. Он будет выполнен вне зависимости от того, была ли операция успешной или завершилась ошибкой.
Самый частый сценарий использования finally
— работа с индикаторами загрузки. Перед началом асинхронной операции разработчик включает индикатор загрузки. Индикатор нужно убрать вне зависимости от того, как завершилась операция. Если этого не сделать, то пользователь не сможет взаимодействовать с интерфейсом.
Метод finally
принимает в качестве аргумента функцию-колбэк, которая выполняется сразу после того, как промис поменял состояние на rejected
или fulfilled
:
let isLoading = truefetch(`https://swapi.dev/api/films/${id}/`).finally(function () { isLoading = false})
let isLoading = true fetch(`https://swapi.dev/api/films/${id}/`).finally(function () { isLoading = false })
Цепочки методов
СкопированоМетоды then
, catch
и finally
часто объединяют в цепочки вызовов, чтобы обработать и успешный, и ошибочный сценарии:
let isLoading = truefetch(`https://swapi.dev/api/films/${id}/`) .then(function (movies) { renderList(movies) }) .catch(function (err) { renderErrorMessage(err) }) .finally(function () { isLoading = false })
let isLoading = true fetch(`https://swapi.dev/api/films/${id}/`) .then(function (movies) { renderList(movies) }) .catch(function (err) { renderErrorMessage(err) }) .finally(function () { isLoading = false })
В этом случае при успешном завершении операции мы выполним код из then
, при ошибке — код из catch
. Затем выполнится код из finally
.
Цепочки методов — очень гибкий подход. Он позволяет создавать зависимые асинхронные операции.
Например, нужно отобразить информацию о фильме и главном герое. Мы не знаем, кто главный герой, не получив эту информацию из данных о фильме. Таким образом, запрос данных о герое зависит от результата запроса данных о фильме.
Промисы делают решение простым и читаемым. Мы можем начинать следующее асинхронное действие внутри колбэка метода then
. Все, что возвращается из колбэка, оборачивается в промис, поэтому в цепочку можно добавить новый then
:
fetch(`https://swapi.dev/api/films/${id}/`) .then(function (response) { // этот then сработает, когда разрешится промис с запросом данных о фильме return response.json() // нужно распарсить ответ сервера, это асинхронная операция }) .then(function (movie) { // этот then сработает, когда данные о фильме распарсятся const characterUrl = movie.characters[0] return fetch(characterUrl) // вызов fetch вернет промис, возвращаем его из колбэка, чтобы продолжить цепочку }) .then(function (response) { // этот then сработает, когда разрешится промис с результатами запроса персонажа return response.json() }) .then(function (character) { renderCharacterProfile(character) }) .catch(function (err) { // catch сработает, когда любая из операций выше завершится ошибкой renderErrorMessage(err) })
fetch(`https://swapi.dev/api/films/${id}/`) .then(function (response) { // этот then сработает, когда разрешится промис с запросом данных о фильме return response.json() // нужно распарсить ответ сервера, это асинхронная операция }) .then(function (movie) { // этот then сработает, когда данные о фильме распарсятся const characterUrl = movie.characters[0] return fetch(characterUrl) // вызов fetch вернет промис, возвращаем его из колбэка, чтобы продолжить цепочку }) .then(function (response) { // этот then сработает, когда разрешится промис с результатами запроса персонажа return response.json() }) .then(function (character) { renderCharacterProfile(character) }) .catch(function (err) { // catch сработает, когда любая из операций выше завершится ошибкой renderErrorMessage(err) })
Обработка ошибок в цепочках методов
СкопированоЦепочки then
при обработке промисов могут быть очень большими. В примере выше цепочка состоит из четырёх then
и одного catch
. Как в этом случае отработает catch
?
☝️ catch
обрабатывает ошибки от всех then
между ним и предыдущим catch
.
В примере выше наш catch
— последний, а предыдущего нет, поэтому он будет обрабатывать все ошибки.
Если в цепочке несколько catch
, то каждый ловит ошибки от then
, находящихся выше.
Возможен вариант, когда финального catch
нет. Тогда ошибки от последних then
не будут обрабатываться.
⚠️ Такой код — плохой. Если в одном из последних then
произойдёт ошибка, то вся дальнейшая цепочка не отработает, при этом, из-за асинхронной природы промиса, прочий код вне промиса продолжит работать и приложение не упадёт.
Другие полезные методы промисов
СкопированоИногда вам нужно обернуть уже известный результат вычисления в промис. Для этого вы можете использовать метод Promise
:
const happyDog = Promise.resolve('🐶')happyDog.then(function (dog) { console.log(dog) // 🐶})
const happyDog = Promise.resolve('🐶') happyDog.then(function (dog) { console.log(dog) // 🐶 })
Кроме этого есть метод Promise
, он используется реже. Обратите внимание, что результатом выполнения sad
будет промис в статусе fulfilled
const sadDog = Promise.reject('🐶')sadDog.catch(function (dog) { console.log(dog) // 🐶})
const sadDog = Promise.reject('🐶') sadDog.catch(function (dog) { console.log(dog) // 🐶 })
Как создать асинхронную функцию с промисом
Скопировано- Создать функцию, которая будет выполнять асинхронную операцию:
function earnAllMoney() {}
function earnAllMoney() {}
- Вернуть из функции свежесозданный промис:
function earnAllMoney() { return new Promise(function (resolve, reject) { /* ... */ })}
function earnAllMoney() { return new Promise(function (resolve, reject) { /* ... */ }) }
- Аргументом в конструктор передать функцию, которая выполняет асинхронную операцию и переводит промис в состояние «успех» или «ошибка» в зависимости от результата:
function earnAllMoney() { return new Promise(function (resolve, reject) { const result = tryEarnAllMoney() // асинхронная операция if (result.ok) { resolve(result) // успех → переводим промис в fulfilled и передаём результат } else { reject(new Error(result)) // ошибка → переводим промис в rejected } })}
function earnAllMoney() { return new Promise(function (resolve, reject) { const result = tryEarnAllMoney() // асинхронная операция if (result.ok) { resolve(result) // успех → переводим промис в fulfilled и передаём результат } else { reject(new Error(result)) // ошибка → переводим промис в rejected } }) }
💡 Если асинхронная операция работает через колбэки, то её стоит обернуть в промис, чтобы писать более читаемый код.
Рассмотрим пример с функцией get
, которая принимает два колбэка: первый вызывается при успехе, второй — при ошибке. Код этой функции может выглядеть так:
function getData(onSuccess, onError) { setTimeout(function () { const result = Math.random() if (result > 0.5) { onSuccess(result) } else { onError(new Error('Что-то пошло не так')) } }, 1000)}
function getData(onSuccess, onError) { setTimeout(function () { const result = Math.random() if (result > 0.5) { onSuccess(result) } else { onError(new Error('Что-то пошло не так')) } }, 1000) }
Завернём её в промис:
function promisifiedGetData() { return new Promise(function (resolve, reject) { const result = getData( function (result) { resolve(result) }, function (error) { reject(error) } ) })}
function promisifiedGetData() { return new Promise(function (resolve, reject) { const result = getData( function (result) { resolve(result) }, function (error) { reject(error) } ) }) }
Теперь можно использовать методы then
и catch
:
promisifiedGetData() .then(function () { console.log('success') }) .catch(function (err) { console.error(err.message) })
promisifiedGetData() .then(function () { console.log('success') }) .catch(function (err) { console.error(err.message) })
Промисы схлопываются
СкопированоИнтересная и удобная особенность промисов – если вложить один промис в другой они схлопнутся в один. Например:
const promise = Promise.resolve(Promise.resolve(Promise.resolve('🐶')))// Promise {<fulfilled>: '🐶'}promise.then(console.log)// 🐶
const promise = Promise.resolve(Promise.resolve(Promise.resolve('🐶'))) // Promise {<fulfilled>: '🐶'} promise.then(console.log) // 🐶
На практике
Скопированосоветует Скопировано
🛠 Промис становится «разрешённым» или «завершённым», когда он переходит из состояние pending
в fulfilled
или rejected
. Состояние завершённого промиса нельзя поменять.
const promise = new Promise(function (resolve, reject) { resolve() // в этот момент промис переходит в состояние fulfilled reject() // этот вызов игнорируется, потому что промис разрешился})
const promise = new Promise(function (resolve, reject) { resolve() // в этот момент промис переходит в состояние fulfilled reject() // этот вызов игнорируется, потому что промис разрешился })
🛠 Всегда завершайте использование промиса методом catch
. Если этого не сделать, то следующие промисы в цепочке перестанут работать, и такую ошибку получится поймать только через специальный обработчик – unhandledrejection
.
🛠 Время от времени нужно выполнить несколько асинхронных функций и дождаться, пока все выполнятся или одна из них завершится ошибкой. Для этого существует статический метод Promise
(он возвращает промис).
🛠 Если нужно дождаться пока несколько асинхронных функций завершатся (без разницы, успешно или ошибкой), используйте метод Promise
(вернёт промис).
На собеседовании
СкопированоЭто вопрос без ответа. Вы можете помочь! Почитайте о том, как контрибьютить в Доку.