Промисы JavaScript
Промисы JavaScript: состояния pending, fulfilled и rejected, функция-исполнитель с resolve и reject, методы then/catch/finally и очередь микрозадач.
Промисы в JavaScript — мощный инструмент для управления асинхронными операциями, позволяющий писать более чистый и надёжный код. В этой главе объясняется, что такое промис, три состояния, в которых он может находиться, как работает функция-исполнитель с resolve и reject, как получать результаты с помощью .then, .catch и .finally, и когда именно выполняются колбэки промисов (очередь микрозадач). Понимание этих основ необходимо перед переходом к цепочкам промисов, Promise API и async/await.
Введение в промисы JavaScript
Promise — это object, представляющий значение, которое может быть ещё недоступно, но будет получено в будущем. Вместо того чтобы передавать колбэк в асинхронную функцию и надеяться, что он будет вызван, вы немедленно получаете object промиса и прикрепляете к нему колбэки. Это позволяет избежать глубоко вложенных колбэков, нередко называемых «callback hell», и даёт единый последовательный способ обработки как успеха, так и неудачи.
Типичная асинхронная задача — сетевой запрос, таймер, чтение файла — не имеет готового результата немедленно. Промис служит заглушкой для этого результата.
Три состояния промиса
Промис всегда находится ровно в одном из трёх состояний:
- pending — начальное состояние; операция ещё не завершена.
- fulfilled — операция завершилась успешно, и промис имеет значение.
- rejected — операция завершилась неудачей, и промис имеет причину (обычно
Error).
Промис в состоянии pending может перейти либо в fulfilled, либо в rejected. После этого он становится settled и не может изменить состояние снова. Эта однонаправленная и однократная смена состояния делает промисы предсказуемыми: колбэк .then, прикреплённый к уже выполненному промису, всё равно будет запущен, а промис никогда не может переключиться из fulfilled обратно в rejected.
┌─────────────┐ resolve(value) ┌─────────────┐
│ pending │ ────────────────▶ │ fulfilled │
new Promise ─▶ │ │ └─────────────┘
│ │ reject(reason) ┌─────────────┐
└─────────────┘ ────────────────▶ │ rejected │
└─────────────┘Создание промиса
Чтобы создать промис, нужно вызвать конструктор Promise и передать ему функцию, называемую исполнителем (executor). Исполнитель запускается немедленно и синхронно в момент создания промиса. Он получает в качестве аргументов две функции, которые по соглашению называют resolve и reject:
- Вызовите
resolve(value), чтобы выполнить промис со значениемvalue. - Вызовите
reject(reason), чтобы отклонить промис с причинойreason.
До вызова одной из них промис остаётся в состоянии pending.
Исполнитель получает resolve и reject, чтобы завершить промис после окончания асинхронной работы. Здесь промис переходит в состояние fulfilled через таймер в одну секунду:
Примечание: Значение имеет только первый вызов
resolveилиreject. После завершения промиса любые последующие вызовыresolveилиrejectигнорируются. Кроме того, если исполнитель синхронно бросает ошибку, промис автоматически отклоняется с этой ошибкой.
Обработка результатов с помощью .then, .catch и .finally
После создания промиса его результат потребляется с помощью методов .then, .catch и .finally. Именно так остальной код реагирует на завершённый промис.
Метод then
Метод .then используется для планирования колбэка, который выполняется при выполнении промиса. Чтобы промис перешёл в состояние fulfilled, должен быть вызван метод resolve. Аргумент, переданный в метод resolve, станет итоговым значением промиса.
В этом коде промис перейдёт в состояние fulfilled только после истечения таймаута в 1000 мс, когда метод resolve будет вызван со значением "Done!".
Функция в части then выполняется только после вызова метода resolve.
.then может также принимать второй аргумент — обработчик отклонения — но использование отдельного .catch (описано ниже) нагляднее и перехватывает ошибки из предыдущих обработчиков тоже.
Метод .catch
Метод .catch используется для обработки промиса в случае его отклонения. Это происходит либо при броске ошибки в блоке функции промиса, либо при вызове метода reject.
Более подробные паттерны обработки ошибок — отклонение против брошенных ошибок, повторный бросок и восстановление внутри цепочки — см. в Обработка ошибок с промисами.
Метод .finally
Метод .finally позволяет выполнить код после завершения промиса независимо от его результата. Он не получает аргументов (не знает, был ли промис fulfilled или rejected) и передаёт результат или ошибку без изменений, поэтому идеально подходит для очистки, например скрытия индикатора загрузки.
Когда выполняются колбэки промисов? (Микрозадачи)
Распространённый сюрприз заключается в том, что колбэки .then, .catch и .finally никогда не выполняются синхронно, даже если промис уже завершён. Они планируются в очереди микрозадач, которую движок обрабатывает только после завершения текущего синхронного кода.
Это означает, что синхронный код всегда выполняется первым, а колбэки промисов — до таймеров (setTimeout), которые находятся в отдельной очереди макрозадач.
Несмотря на то что промис разрешается немедленно, а таймаут равен 0, колбэк промиса (3) выполняется раньше колбэка таймаута (4), поскольку очередь микрозадач полностью опустошается до выполнения следующей макрозадачи.
Получение данных из API с помощью промисов
Этот пример показывает, как получить данные из удалённого API с помощью промисов.
Цепочки промисов
Цепочки промисов — мощная возможность, позволяющая связывать несколько асинхронных операций последовательно. Каждый .then возвращает новый промис, и то, что вы возвращаете (return) из обработчика, становится значением выполнения этого нового промиса — именно поэтому в примере ниже значение передаётся через несколько шагов. Подробнее см. в JavaScript: промисы и цепочки.
Использование async/await с промисами JavaScript
Применение async/await позволяет упростить обработку асинхронных операций, делая код чище и понятнее, сохраняя при этом всю мощь промисов JavaScript. Подробнее об этом вы узнаете в разделе JavaScript async/await, а пока — простой пример.
Заключение
Владение промисами JavaScript необходимо любому разработчику, который хочет эффективно управлять асинхронными операциями. Запомните основную модель: промис находится в состоянии pending до тех пор, пока исполнитель не вызовет resolve или reject, после чего он навсегда становится settled; результат читается с помощью .then/.catch/.finally; а эти колбэки всегда выполняются как микрозадачи, после завершения текущего синхронного кода.
Что изучить дальше
- JavaScript: промисы и цепочки — последовательность асинхронных шагов и передача значений.
- Promise API — статические вспомогательные методы:
Promise.all,Promise.race,Promise.allSettledиPromise.resolve. - Обработка ошибок с промисами — отклонения, брошенные ошибки и паттерны восстановления.
- JavaScript async/await — написание кода на основе промисов, который читается как синхронный.