- Операторы
- Управляющие инструкции
- JS Объекты
- браузер BOM
- HTML DOM
- События
- HTML Объекты
- Промисы, async/await
- Колбэки
- Промисы
- Цепочка промисов
- Обработка ошибок
- Promise API
- Промисификация
- Микрозадачи
- Async/await
- Сетевые запросы
- Бинарные данные и файлы
- Модули
- Классы
- Разное
Промисы
Представьте, что вы известный певец, которого фанаты постоянно донимают расспросами о предстоящем сингле.
Чтобы получить передышку, вы обещаете разослать им сингл, когда он будет выпущен. Вы даёте фанатам список, в который они могут записаться. Они могут оставить там свой e-mail, чтобы получить песню, как только она выйдет. И даже больше: если что-то пойдёт не так, например, в студии будет пожар и песню выпустить не выйдет, они также получат уведомление об этом.
Все счастливы! Вы счастливы, потому что вас больше не донимают фанаты, а фанаты могут больше не беспокоиться, что пропустят новый сингл.
Это аналогия из реальной жизни для ситуаций, с которыми мы часто сталкиваемся в программировании:
- Есть «создающий» код, который делает что-то, что занимает время. Например, загружает данные по сети. В нашей аналогии это – «певец».
- Есть «потребляющий» код, который хочет получить результат «создающего» кода, когда он будет готов. Он может быть необходим более чем одной функции. Это – «фанаты».
Promise
(по англ.promise
, будем называть такой объект «промис») – это специальный объект в JavaScript, который связывает «создающий» и «потребляющий» коды вместе. В терминах нашей аналогии – это «список для подписки». «Создающий» код может выполняться сколько потребуется, чтобы получить результат, а промис делает результат доступным для кода, который подписан на него, когда результат готов.
Аналогия не совсем точна, потому что объект Promise
в
JavaScript гораздо сложнее простого списка подписок: он обладает
дополнительными возможностями и ограничениями. Но для начала и такая
аналогия хороша.
Синтаксис создания Promise
:
let promise = new Promise(function(resolve, reject) { // функция-исполнитель (executor) // "певец" });
Функция, переданная в конструкцию new Promise
, называется исполнитель (executor). Когда Promise
создаётся, она запускается автоматически. Она должна содержать
«создающий» код, который когда-нибудь создаст результат. В терминах
нашей аналогии: исполнитель – это «певец».
Её аргументы resolve
и reject
– это колбэки, которые предоставляет сам JavaScript. Наш код – только внутри исполнителя.
Когда он получает результат, сейчас или позже – не важно, он должен вызвать один из этих колбэков:
resolve(value)
— если работа завершилась успешно, с результатомvalue
.reject(error)
— если произошла ошибка,error
– объект ошибки.
Итак, исполнитель запускается автоматически, он должен выполнить работу, а затем вызвать resolve
или reject
.
У объекта promise
, возвращаемого конструктором new Promise
, есть внутренние свойства:
state
(«состояние») — вначале"pending"
(«ожидание»), потом меняется на"fulfilled"
(«выполнено успешно») при вызовеresolve
или на"rejected"
(«выполнено с ошибкой») при вызовеreject
.result
(«результат») — вначалеundefined
, далее изменяется наvalue
при вызовеresolve(value)
или наerror
при вызовеreject(error)
.
Так что исполнитель по итогу переводит promise
в одно из двух состояний:
Позже мы рассмотрим, как «фанаты» узнают об этих изменениях.
Ниже пример конструктора Promise
и простого исполнителя с кодом, дающим результат с задержкой (через setTimeout
):
let promise = new Promise(function(resolve, reject) { // эта функция выполнится автоматически, при вызове new Promise // через 1 секунду сигнализировать, что задача выполнена с результатом "done" setTimeout(() => resolve("done"), 1000); });
Мы можем наблюдать две вещи, запустив код выше:
- Функция-исполнитель запускается сразу же при вызове
new Promise
. - Исполнитель получает два аргумента:
resolve
иreject
— это функции, встроенные в JavaScript, поэтому нам не нужно их писать. Нам нужно лишь позаботиться, чтобы исполнитель вызвал одну из них по готовности.
Спустя одну секунду «обработки» исполнитель вызовет resolve("done")
, чтобы передать результат:
Это был пример успешно выполненной задачи, в результате мы получили «успешно выполненный» промис.
А теперь пример, в котором исполнитель сообщит, что задача выполнена с ошибкой:
let promise = new Promise(function(resolve, reject) { // спустя одну секунду будет сообщено, что задача выполнена с ошибкой setTimeout(() => reject(new Error("Whoops!")), 1000); });
Подведём промежуточные итоги: исполнитель выполняет задачу (что-то, что обычно требует времени), затем вызывает resolve
или reject
, чтобы изменить состояние соответствующего Promise
.
Промис – и успешный, и отклонённый будем называть «завершённым», в отличие от изначального промиса «в ожидании».
Может быть что-то одно: либо результат, либо ошибка
Исполнитель должен вызвать что-то одно: resolve
или reject
. Состояние промиса может быть изменено только один раз.
Все последующие вызовы resolve
и reject
будут проигнорированы:
let promise = new Promise(function(resolve, reject) { resolve("done"); reject(new Error("…")); // игнорируется setTimeout(() => resolve("…")); // игнорируется });
Идея в том, что задача, выполняемая исполнителем, может иметь только один итог: результат или ошибку.
Также заметим, что функция resolve
/reject
ожидает только один аргумент (или ни одного). Все дополнительные аргументы будут проигнорированы.
Вызывайте
reject
с объектом Error
В случае, если что-то пошло не так, мы должны вызвать reject
. Это можно сделать с аргументом любого типа (как и resolve
), но рекомендуется использовать объект Error
(или унаследованный от него). Почему так? Скоро нам станет понятно.
Вызов
resolve
/reject
сразу
Обычно исполнитель делает что-то асинхронное и после этого вызывает resolve
/reject
, то есть через какое-то время. Но это не обязательно, resolve
или reject
могут быть вызваны сразу:
let promise = new Promise(function(resolve, reject) { // задача, не требующая времени resolve(123); // мгновенно выдаст результат: 123 });
Это может случиться, например, когда мы начали выполнять какую-то задачу, но тут же увидели, что ранее её уже выполняли, и результат закеширован.
Такая ситуация нормальна. Мы сразу получим успешно завершённый Promise
.
Свойства
state
и result
– внутренние
Свойства state
и result
– это внутренние свойства объекта Promise
и мы не имеем к ним прямого доступа. Для обработки результата следует использовать методы .then
/.catch
/.finally
, про них речь пойдёт дальше.
Потребители: then, catch, finally
Объект Promise
служит связующим звеном между исполнителем («создающим» кодом или
«певцом») и функциями-потребителями («фанатами»), которые получат либо
результат, либо ошибку. Функции-потребители могут быть зарегистрированы
(подписаны) с помощью методов .then
, .catch
и .finally
.
then
Наиболее важный и фундаментальный метод – .then
.
Синтаксис:
promise.then( function(result) { /* обработает успешное выполнение */ }, function(error) { /* обработает ошибку */ } );
Первый аргумент метода .then
– функция, которая выполняется, когда промис переходит в состояние «выполнен успешно», и получает результат.
Второй аргумент .then
– функция, которая выполняется, когда промис переходит в состояние «выполнен с ошибкой», и получает ошибку.
Например, вот реакция на успешно выполненный промис:
let promise = new Promise(function(resolve, reject) { setTimeout(() => resolve("done!"), 1000); }); // resolve запустит первую функцию, переданную в .then promise.then( result => Alert(result), // выведет "done!" через одну секунду error => Alert(error) // не будет запущена );
Выполнилась первая функция.
А в случае ошибки в промисе – выполнится вторая:
let promise = new Promise(function(resolve, reject) { setTimeout(() => reject(new Error("Whoops!")), 1000); }); // reject запустит вторую функцию, переданную в .then promise.then( result => Alert(result), // не будет запущена error => Alert(error) // выведет "Error: Whoops!" спустя одну секунду );
Если мы заинтересованы только в результате успешного выполнения задачи, то в then
можно передать только одну функцию:
let promise = new Promise(resolve => { setTimeout(() => resolve("done!"), 1000); }); promise.then(alert); // выведет "done!" спустя одну секунду
catch
Если мы хотели бы только обработать ошибку, то можно использовать null
в качестве первого аргумента: .then(null, errorHandlingFunction)
. Или можно воспользоваться методом .catch(errorHandlingFunction)
, который сделает то же самое:
let promise = new Promise((resolve, reject) => { setTimeout(() => reject(new Error("Ошибка!")), 1000); }); // .catch(f) это то же самое, что promise.then(null, f) promise.catch(alert); // выведет "Error: Ошибка!" спустя одну секунду
Вызов .catch(f)
– это сокращённый, «укороченный» вариант .then(null, f)
.
finally
По аналогии с блоком finally
из обычного try {...} catch {...}
, у промисов также есть метод finally
.
Вызов .finally(f)
похож на .then(f, f)
, в том смысле, что f
выполнится в любом случае, когда промис завершится: успешно или с ошибкой.
finally
хорошо подходит для очистки, например остановки индикатора загрузки, его ведь нужно остановить вне зависимости от результата.
Например:
new Promise((resolve, reject) => { /* сделать что-то, что займёт время, и после вызвать resolve/reject */ }) // выполнится, когда промис завершится, независимо от того, успешно или нет .finally(() => остановить индикатор загрузки) .then(result => показать результат, err => показать ошибку)
Но это не совсем псевдоним then(f,f)
, как можно было подумать. Существует несколько важных отличий:
-
Обработчик, вызываемый из
finally
, не имеет аргументов. Вfinally
мы не знаем, как был завершён промис. И это нормально, потому что обычно наша задача – выполнить «общие» завершающие процедуры. -
Обработчик
finally
«пропускает» результат или ошибку дальше, к последующим обработчикам.Например, здесь результат проходит через
finally
кthen
:new Promise((resolve, reject) => { setTimeout(() => resolve("result"), 2000) }) .finally(() => Alert("Промис завершён")) .then(result => Alert(result)); // <-- .then обработает результат
А здесь ошибка из промиса проходит через
finally
кcatch
:new Promise((resolve, reject) => { throw new Error("error"); }) .finally(() => Alert("Промис завершён")) .catch(err => Alert(err)); // <-- .catch обработает объект ошибки
Это очень удобно, потому что
finally
не предназначен для обработки результата промиса. Так что он просто пропускает его через себя дальше.Мы более подробно поговорим о создании цепочек промисов и передаче результатов между обработчиками в следующей главе.
-
Последнее, но не менее значимое: вызов
.finally(f)
удобнее, чем.then(f, f)
– не надо дублировать функции f.
На завершённых промисах обработчики запускаются сразу
Если промис в состоянии ожидания, обработчики в .then/catch/finally
будут ждать его. Однако, если промис уже завершён, то обработчики выполнятся сразу:
// при создании промиса он сразу переводится в состояние "успешно завершён" let promise = new Promise(resolve => resolve("готово!")); promise.then(alert); // готово! (выведется сразу)
Теперь рассмотрим несколько практических примеров того, как промисы могут облегчить нам написание асинхронного кода.
Пример: loadScript
У нас есть функция loadScript
для загрузки скрипта из предыдущей главы.
Давайте вспомним, как выглядел вариант с колбэками:
function loadScript(src, callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); script.onerror = () => callback(new Error(`Ошибка загрузки скрипта ${src}`)); document.head.append(script); }
Теперь перепишем её, используя Promise
.
Новой функции loadScript
более не нужен аргумент callback
. Вместо этого она будет создавать и возвращать объект Promise
,
который перейдет в состояние «успешно завершён», когда загрузка
закончится. Внешний код может добавлять обработчики («подписчиков»),
используя .then
:
function loadScript(src) { return new Promise(function(resolve, reject) { let script = document.createElement('script'); script.src = src; script.onload = () => resolve(script); script.onerror = () => reject(new Error(`Ошибка загрузки скрипта ${src}`)); document.head.append(script); }); }
Применение:
function loadScript(src) { return new Promise(function(resolve, reject) { let script = document.createElement('script'); script.src = src; script.onload = () => resolve(script); script.onerror = () => reject(new Error(`Ошибка загрузки скрипта ${src}`)); document.head.append(script); }); } let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); promise.then( script => Alert(`${script.src} загружен!`), error => Alert(`Ошибка: ${error.message}`) ); promise.then(script => Alert('Ещё один обработчик...'));
Сразу заметно несколько преимуществ перед подходом с использованием колбэков:
Промисы | Колбэки |
---|---|
Промисы позволяют делать вещи в естественном порядке. Сперва мы запускаем loadScript(script) , и затем (.then ) мы пишем, что делать с результатом. |
У нас должна быть функцияcallback на момент вызова loadScript(script, callback) . Другими словами, нам нужно знать что делать с результатом до того, как вызовется loadScript . |
Мы можем вызывать .then у Promise столько
раз, сколько захотим. Каждый раз мы добавляем нового «фаната», новую
функцию-подписчика в «список подписок». Больше об этом в следующей
главе: Цепочка промисов. |
Колбэк может быть только один. |
Таким образом, промисы позволяют улучшить порядок кода и дают нам гибкость.
Источник: learn.javascript.ru/promise-basics