Представьте, что вы популярный певец, а фанаты день и ночь спрашивают о вашей новой песне.
Чтобы получить некоторое облегчение, вы обещаете отправить им статью, как только она будет опубликована. Вы даете своим поклонникам список. Они могут указать свои адреса электронной почты, чтобы, когда песня станет доступной, все подписчики мгновенно получили ее. И даже если что-то пойдет не так, например, пожар в студии, и вы не сможете опубликовать песню, они все равно будут уведомлены.
Все счастливы: ты, потому что народ тебя больше не толпит, и фанаты, потому что они не пропустят песню.
Это реальная аналогия вещей, которые мы часто встречаем в программировании:
«Производящий код», который что-то делает и требует времени. Например, некоторый код, который загружает данные по сети. Это «певец».
«Потребляющий код», которому нужен результат «производящего кода», как только он будет готов. Многим функциям может потребоваться этот результат. Это «фанаты».
Промис — это специальный объект JavaScript, который связывает «производящий код» и «потребляющий код» вместе. Если использовать нашу аналогию: это «список подписки». «Производящему коду» требуется столько времени, сколько необходимо для получения обещанного результата, а «обещание» делает этот результат доступным для всего подписанного кода, когда он будет готов.
Аналогия не совсем точна, поскольку обещания JavaScript более сложны, чем простой список подписок: у них есть дополнительные функции и ограничения. Но для начала это нормально.
Синтаксис конструктора объекта обещания:
пусть обещание = новое обещание (функция (разрешить, отклонить) { // исполнитель (производящий код, "певец") });
Функция, передаваемая в new Promise
называется исполнителем . Когда создается new 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
в одно из следующих состояний:
Позже мы увидим, как «фанаты» смогут подписаться на эти изменения.
Вот пример конструктора обещаний и простой функции-исполнителя с «генерацией кода», требующей времени (через setTimeout
):
пусть обещание = новое обещание (функция (разрешить, отклонить) { // функция выполняется автоматически при создании промиса // через 1 секунду сигнал о том, что задание выполнено с результатом «done» setTimeout(() =>solve("готово"), 1000); });
Мы можем увидеть две вещи, запустив приведенный выше код:
Исполнитель вызывается автоматически и немедленно (по new Promise
).
Исполнитель получает два аргумента: resolve
и reject
. Эти функции предопределены движком JavaScript, поэтому нам не нужно их создавать. Мы должны позвонить только одному из них, когда будем готовы.
После одной секунды «обработки» исполнитель вызывает resolve("done")
для получения результата. Это меняет состояние объекта promise
:
Это был пример успешного завершения работы, «выполненного обещания».
А теперь пример отказа исполнителя от обещания с ошибкой:
пусть обещание = новое обещание (функция (разрешить, отклонить) { // через 1 секунду сигнал о том, что задание завершено с ошибкой setTimeout(() => ignore(new Error("Упс!")), 1000); });
Вызов reject(...)
переводит объект обещания в состояние "rejected"
:
Подводя итог, исполнитель должен выполнить задание (обычно это требует времени), а затем вызвать resolve
или reject
, чтобы изменить состояние соответствующего объекта обещания.
Обещание, которое либо решено, либо отклонено, называется «исполненным», в отличие от первоначально «ожидающего» обещания.
Может быть только один результат или ошибка
Исполнитель должен вызывать только одно resolve
или одно reject
. Любое изменение состояния является окончательным.
Все дальнейшие вызовы resolve
и reject
игнорируются:
пусть обещание = новое обещание (функция (разрешить, отклонить) { решить("готово"); отклонить (новая ошибка («…»)); // игнорируется setTimeout(() =>solve("…")); // игнорируется });
Идея состоит в том, что работа, выполняемая исполнителем, может иметь только один результат или ошибку.
Кроме того, resolve
/ reject
ожидает только один аргумент (или ни одного) и игнорирует дополнительные аргументы.
Отклонить с объектами Error
В случае, если что-то пойдет не так, исполнитель должен вызвать reject
. Это можно сделать с любым типом аргумента (точно так же, как resolve
). Но рекомендуется использовать объекты Error
(или объекты, наследуемые от Error
). Причина этого вскоре станет очевидна.
Немедленный вызов resolve
/ reject
На практике исполнитель обычно делает что-то асинхронно и через некоторое время resolve
/ reject
, но это не обязательно. Мы также можем немедленно вызвать resolve
или reject
, например:
пусть обещание = новое обещание (функция (разрешить, отклонить) { // не тратим время на выполнение работы решить(123); // сразу выдаем результат: 123 });
Например, это может произойти, когда мы начинаем выполнять работу, но затем видим, что все уже выполнено и кэшировано.
Это нормально. У нас сразу же есть решенное обещание.
state
и result
являются внутренними
state
свойств и result
объекта Promise являются внутренними. Мы не можем получить к ним прямой доступ. Для этого мы можем использовать методы .then
/ .catch
/ .finally
. Они описаны ниже.
Объект Promise служит связующим звеном между исполнителем («производящим кодом» или «исполнителем») и функциями-потребителями («фанатами»), которые получат результат или ошибку. Потребляющие функции можно зарегистрировать (подписаться) с помощью методов .then
и .catch
.
Самый важный, фундаментальный из них — .then
.
Синтаксис:
обещание.тогда( function(result) { /* обрабатываем успешный результат */ }, function(error) { /* обрабатываем ошибку */ } );
Первый аргумент .then
— это функция, которая запускается, когда обещание разрешено, и получает результат.
Второй аргумент .then
— это функция, которая запускается, когда обещание отклонено и получена ошибка.
Например, вот реакция на успешно выполненное обещание:
пусть обещание = новое обещание (функция (разрешить, отклонить) { setTimeout(() =>solve("готово!"), 1000); }); // решение запускает первую функцию в .then обещание.тогда( result => alert(result), // показывает «готово!» через 1 секунду error => alert(error) // не запускается );
Первая функция была выполнена.
А в случае отказа второй:
пусть обещание = новое обещание (функция (разрешить, отклонить) { setTimeout(() => ignore(new Error("Упс!")), 1000); }); // отклонить запускает вторую функцию в .then обещание.тогда( result => alert(result), // не запускается error => alert(error) // показывает «Ошибка: упс!» через 1 секунду );
Если нас интересуют только успешные завершения, мы можем предоставить только один аргумент функции .then
:
пусть обещание = новое обещание (решить => { setTimeout(() =>solve("готово!"), 1000); }); обещание.тогда(предупреждение); // показывает "готово!" через 1 секунду
Если нас интересуют только ошибки, мы можем использовать null
в качестве первого аргумента: .then(null, errorHandlingFunction)
. Или мы можем использовать .catch(errorHandlingFunction)
, что абсолютно то же самое:
пусть обещание = новое обещание ((разрешить, отклонить) => { setTimeout(() => ignore(new Error("Упс!")), 1000); }); // .catch(f) — то же самое, что и обещание.then(null, f) обещание.catch(предупреждение); // показывает «Ошибка: Упс!» через 1 секунду
Вызов .catch(f)
— полный аналог .then(null, f)
, это просто сокращение.
Точно так же, как в обычном операторе try {...} catch {...}
есть finally
, оно есть finally
в промисах.
Вызов .finally(f)
похож на .then(f, f)
в том смысле, что f
выполняется всегда, когда обещание выполнено: будь то разрешение или отклонение.
Идея, finally
состоит в том, чтобы настроить обработчик для выполнения очистки/финализации после завершения предыдущих операций.
Например, остановка загрузки индикаторов, закрытие ненужных соединений и т. д.
Думайте об этом как о завершении вечеринки. Неважно, хорошая или плохая вечеринка, сколько бы друзей на ней ни было, нам все равно нужно (или хотя бы стоит) устроить после нее уборку.
Код может выглядеть так:
новое обещание((разрешить, отклонить) => { /* делаем что-то, что требует времени, а затем вызываем функцию разрешения или, возможно, отклонения */ }) // запускается, когда обещание выполнено, не имеет значения, успешно оно или нет .finally(() => индикатор остановки загрузки) // поэтому индикатор загрузки всегда останавливается, прежде чем мы продолжим .then(result => показать результат, err => показать ошибку)
Обратите внимание: « finally(f)
— это не совсем псевдоним then(f,f)
.
Есть важные различия:
Обработчик finally
не имеет аргументов. В finally
мы не знаем, будет ли обещание успешным или нет. Ничего страшного, ведь наша задача обычно заключается в выполнении «общих» процедур финализации.
Пожалуйста, взгляните на приведенный выше пример: как вы можете видеть, обработчик finally
не имеет аргументов, а результат обещания обрабатывается следующим обработчиком.
Обработчик « finally
» «передаёт» результат или ошибку следующему подходящему обработчику.
Например, здесь результат finally
передается then
:
новое обещание((разрешить, отклонить) => { setTimeout(() =>solve("значение"), 2000); }) .finally(() => alert("Обещание готово")) // срабатывает первым .then(результат => оповещение(результат)); // <-- .тогда показывает "значение"
Как видите, value
, возвращаемое первым обещанием, finally
передается следующему then
.
Это очень удобно, потому что finally
не предназначена для обработки обещанного результата. Как уже было сказано, это место для общей очистки, независимо от результата.
А вот пример ошибки, чтобы мы могли увидеть, как она finally
catch
:
новое обещание((разрешить, отклонить) => { выдать новую ошибку («ошибка»); }) .finally(() => alert("Обещание готово")) // срабатывает первым .catch(err => alert(err)); // <-- .catch показывает ошибку
finally
также не должен ничего возвращать. Если это так, возвращаемое значение игнорируется.
Единственным исключением из этого правила является ситуация, когда finally
выдает ошибку. Затем эта ошибка передается следующему обработчику вместо любого предыдущего результата.
Подводя итог:
finally
не получает результат предыдущего обработчика (у него нет аргументов). Вместо этого этот результат передается следующему подходящему обработчику.
Если обработчик finally
возвращает что-то, это игнорируется.
Когда finally
выдается ошибка, выполнение переходит к ближайшему обработчику ошибок.
Эти функции полезны и заставляют все работать правильно, если мы finally
используем их так, как их следует использовать: для общих процедур очистки.
Мы можем прикрепить обработчики к выполненным промисам.
Если обещание находится на рассмотрении, обработчики .then/catch/finally
ждут его результата.
Иногда может случиться так, что обещание уже выполнено, когда мы добавляем к нему обработчик.
В таком случае эти обработчики запускаются сразу:
// промис становится разрешенным сразу после создания let обещание = новое обещание (решить => разрешить («готово!»)); обещание.тогда(предупреждение); // сделанный! (появляется прямо сейчас)
Обратите внимание, что это делает обещания более действенными, чем реальный сценарий «списка подписки». Если певец уже выпустил свою песню, а затем человек подписывается в список подписки, он, вероятно, не получит эту песню. Подписки в реальной жизни должны быть сделаны до мероприятия.
Обещания более гибкие. Мы можем добавить обработчики в любое время: если результат уже есть, они просто выполняются.
Далее давайте рассмотрим более практические примеры того, как промисы могут помочь нам писать асинхронный код.
У нас есть функция loadScript
для загрузки скрипта из предыдущей главы.
Вот вариант на основе обратного вызова, просто чтобы напомнить нам о нем:
функция loadScript(src, обратный вызов) { пусть скрипт = document.createElement('script'); script.src = источник; script.onload = () => обратный вызов (null, скрипт); script.onerror = () => callback(new Error(`Ошибка загрузки скрипта для ${src}`)); document.head.append(скрипт); }
Давайте перепишем его, используя Promises.
Новая функция loadScript
не потребует обратного вызова. Вместо этого он создаст и вернет объект Promise, который разрешается после завершения загрузки. Внешний код может добавлять к нему обработчики (функции подписки) с помощью .then
:
функция loadScript(src) { вернуть новое обещание (функция (разрешить, отклонить) { пусть скрипт = document.createElement('script'); script.src = источник; script.onload = () => разрешить (скрипт); script.onerror = () => ignore(new Error(`Ошибка загрузки скрипта для ${src}`)); document.head.append(скрипт); }); }
Использование:
let Promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); обещание.тогда( script => alert(`${script.src} загружен!`), error => alert(`Ошибка: ${error.message}`) ); обещание.then(script => alert('Другой обработчик...'));
Мы сразу видим несколько преимуществ по сравнению с шаблоном на основе обратного вызова:
Обещания | Обратные вызовы |
---|---|
Обещания позволяют нам делать вещи в естественном порядке. Сначала запускаем loadScript(script) , а .then пишем, что делать с результатом. | При вызове loadScript(script, callback) в нашем распоряжении должна быть функция callback . Другими словами, мы должны знать, что делать с результатом, прежде чем вызывать loadScript . |
Мы можем вызывать .then для промиса столько раз, сколько захотим. Каждый раз мы добавляем в «список подписки» нового «поклонника», новую функцию подписки. Подробнее об этом в следующей главе: Цепочка промисов. | Обратный вызов может быть только один. |
Таким образом, обещания дают нам лучший поток кода и гибкость. Но это еще не все. Мы увидим это в следующих главах.
Каков вывод кода ниже?
пусть обещание = новое обещание (функция (разрешить, отклонить) { решить(1); setTimeout(() => разрешить(2), 1000); }); обещание.тогда(предупреждение);
Вывод: 1
.
Второй вызов resolve
игнорируется, поскольку учитывается только первый вызов reject/resolve
. Дальнейшие звонки игнорируются.
Встроенная функция setTimeout
использует обратные вызовы. Создайте альтернативу, основанную на обещаниях.
Функция delay(ms)
должна возвращать обещание. Это обещание должно разрешиться через ms
, чтобы мы могли добавить к нему .then
, вот так:
функция задержки (мс) { // ваш код } задержка(3000).then(() => alert('запускается через 3 секунды'));
функция задержки (мс) { вернуть новое обещание (решить => setTimeout (решить, мс)); } задержка(3000).then(() => alert('запускается через 3 секунды'));
Обратите внимание, что в этой задаче resolve
вызывается без аргументов. Мы не возвращаем никакого значения из delay
, просто обеспечиваем задержку.
Перепишите функцию showCircle
в решении задачи Анимированный круг с обратным вызовом так, чтобы она возвращала обещание вместо принятия обратного вызова.
Новое использование:
showCircle(150, 150, 100).then(div => { div.classList.add('шар сообщения'); div.append("Привет, мир!"); });
За основу возьмем решение задачи Анимированный круг с обратным вызовом.
Откройте решение в песочнице.