Промисы
Представьте, что вы известный певец, которого фанаты постоянно донимают расспросами о предстоящем сингле.
Чтобы получить передышку, вы обещаете разослать им сингл, когда он будет выпущен. Вы даёте фанатам список, в который они могут записаться. Они могут оставить там свой 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
Объект Promise служит связующим звеном между исполнителем («создающим» кодом или «певцом») и функциями-потребителями («фанатами»), которые получат либо результат, либо ошибку. Функции-потребители могут быть зарегистрированы (подписаны) с помощью методов .then и .catch .
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 => показать ошибку)
Обратите внимание, что finally(f) – это не совсем псевдоним then(f,f) , как можно было подумать.
Есть важные различия:
- Обработчик, вызываемый из finally , не имеет аргументов. В finally мы не знаем, как был завершён промис. И это нормально, потому что обычно наша задача – выполнить «общие» завершающие процедуры. Пожалуйста, взгляните на приведенный выше пример: как вы можете видеть, обработчик finally не имеет аргументов, а результат promise обрабатывается в следующем обработчике.
- Обработчик finally «пропускает» результат или ошибку дальше, к последующим обработчикам. Например, здесь результат проходит через finally к then :
new Promise((resolve, reject) => < setTimeout(() =>resolve("value"), 2000); >) .finally(() => alert("Промис завершён")) // срабатывает первым .then(result => alert(result)); //
Как вы можете видеть, значение возвращаемое первым промисом, передается через finally к следующему then . Это очень удобно, потому что finally не предназначен для обработки результата промиса. Как уже было сказано, это место для проведения общей очистки, независимо от того, каков был результат. А здесь ошибка из промиса проходит через finally к catch :
new Promise((resolve, reject) => < throw new Error("error"); >) .finally(() => alert("Промис завершён")) // срабатывает первым .catch(err => alert(err)); //
- Обработчик finally не получает результат предыдущего обработчика (у него нет аргументов). Вместо этого этот результат передается следующему подходящему обработчику.
- Если обработчик finally возвращает что-то, это игнорируется.
- Когда finally выдает ошибку, выполнение переходит к ближайшему обработчику ошибок.
Эти функции полезны и заставляют все работать правильно, если мы используем finally так, как предполагается: для общих процедур очистки.
На завершённых промисах обработчики запускаются сразу
Если промис в состоянии ожидания, обработчики в .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(`Ошибка загрузки скрипта $`)); 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(`Ошибка загрузки скрипта $`)); document.head.append(script); >); >
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); promise.then( script => alert(`$ загружен!`), error => alert(`Ошибка: $`) ); promise.then(script => alert('Ещё один обработчик. '));
Сразу заметно несколько преимуществ перед подходом с использованием колбэков:
Промисы | Колбэки |
---|---|
Промисы позволяют делать вещи в естественном порядке. Сперва мы запускаем loadScript(script) , и затем ( .then ) мы пишем, что делать с результатом. | У нас должна быть функция callback на момент вызова loadScript(script, callback) . Другими словами, нам нужно знать что делать с результатом до того, как вызовется loadScript . |
Мы можем вызывать .then у Promise столько раз, сколько захотим. Каждый раз мы добавляем нового «фаната», новую функцию-подписчика в «список подписок». Больше об этом в следующей главе: Цепочка промисов. | Колбэк может быть только один. |
Таким образом, промисы позволяют улучшить порядок кода и дают нам гибкость. Но это далеко не всё. Мы узнаем ещё много полезного в последующих главах.
Promise
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Более новая информация по этой теме находится на странице https://learn.javascript.ru/promise-basics.
Promise (обычно их так и называют «промисы») – предоставляют удобный способ организации асинхронного кода.
В современном JavaScript промисы часто используются в том числе и неявно, при помощи генераторов, но об этом чуть позже.
Что такое Promise?
Promise – это специальный объект, который содержит своё состояние. Вначале pending («ожидание»), затем – одно из: fulfilled («выполнено успешно») или rejected («выполнено с ошибкой»).
На promise можно навешивать колбэки двух типов:
- onFulfilled – срабатывают, когда promise в состоянии «выполнен успешно».
- onRejected – срабатывают, когда promise в состоянии «выполнен с ошибкой».
Способ использования, в общих чертах, такой:
- Код, которому надо сделать что-то асинхронно, создаёт объект promise и возвращает его.
- Внешний код, получив promise , навешивает на него обработчики.
- По завершении процесса асинхронный код переводит promise в состояние fulfilled (с результатом) или rejected (с ошибкой). При этом автоматически вызываются соответствующие обработчики во внешнем коде.
Синтаксис создания Promise :
var promise = new Promise(function(resolve, reject) < // Эта функция будет вызвана автоматически // В ней можно делать любые асинхронные операции, // А когда они завершатся — нужно вызвать одно из: // resolve(результат) при успешном выполнении // reject(ошибка) при ошибке >)
Универсальный метод для навешивания обработчиков:
promise.then(onFulfilled, onRejected)
- onFulfilled – функция, которая будет вызвана с результатом при resolve .
- onRejected – функция, которая будет вызвана с ошибкой при reject .
С его помощью можно назначить как оба обработчика сразу, так и только один:
// onFulfilled сработает при успешном выполнении promise.then(onFulfilled) // onRejected сработает при ошибке promise.then(null, onRejected)
Для того, чтобы поставить обработчик только на ошибку, вместо .then(null, onRejected) можно написать .catch(onRejected) – это то же самое.
Синхронный throw – то же самое, что reject
Если в функции промиса происходит синхронный throw (или иная ошибка), то вызывается reject :
'use strict'; let p = new Promise((resolve, reject) => < // то же что reject(new Error("o_O")) throw new Error("o_O"); >) p.catch(alert); // Error: o_O
Посмотрим, как это выглядит вместе, на простом примере.
Пример с setTimeout
Возьмём setTimeout в качестве асинхронной операции, которая должна через некоторое время успешно завершиться с результатом «result»:
'use strict'; // Создаётся объект promise let promise = new Promise((resolve, reject) => < setTimeout(() =>< // переведёт промис в состояние fulfilled с результатом "result" resolve("result"); >, 1000); >); // promise.then навешивает обработчики на успешный результат или ошибку promise .then( result => < // первая функция-обработчик - запустится при вызове resolve alert("Fulfilled: " + result); // result - аргумент resolve >, error => < // вторая функция - запустится при вызове reject alert("Rejected: " + error); // error - аргумент reject >);
В результате запуска кода выше – через 1 секунду выведется «Fulfilled: result».
А если бы вместо resolve("result") был вызов reject("error") , то вывелось бы «Rejected: error». Впрочем, как правило, если при выполнении возникла проблема, то reject вызывают не со строкой, а с объектом ошибки типа new Error :
// Этот promise завершится с ошибкой через 1 секунду var promise = new Promise((resolve, reject) => < setTimeout(() =>< reject(new Error("время вышло!")); >, 1000); >); promise .then( result => alert("Fulfilled: " + result), error => alert("Rejected: " + error.message) // Rejected: время вышло! );
Конечно, вместо setTimeout внутри функции промиса может быть и запрос к серверу и ожидание ввода пользователя, или другой асинхронный процесс. Главное, чтобы по своему завершению он вызвал resolve или reject , которые передадут результат обработчикам.
Только один аргумент
Функции resolve/reject принимают ровно один аргумент – результат/ошибку.
Именно он передаётся обработчикам в .then , как можно видеть в примерах выше.
Promise после reject/resolve – неизменны
Заметим, что после вызова resolve/reject промис уже не может «передумать».
Когда промис переходит в состояние «выполнен» – с результатом (resolve) или ошибкой (reject) – это навсегда.
'use strict'; let promise = new Promise((resolve, reject) => < // через 1 секунду готов результат: result setTimeout(() =>resolve("result"), 1000); // через 2 секунды — reject с ошибкой, он будет проигнорирован setTimeout(() => reject(new Error("ignored")), 2000); >); promise .then( result => alert("Fulfilled: " + result), // сработает error => alert("Rejected: " + error) // не сработает );
В результате вызова этого кода сработает только первый обработчик then , так как после вызова resolve промис уже получил состояние (с результатом), и в дальнейшем его уже ничто не изменит.
Последующие вызовы resolve/reject будут просто проигнорированы.
А так – наоборот, ошибка будет раньше:
'use strict'; let promise = new Promise((resolve, reject) => < // reject вызван раньше, resolve будет проигнорирован setTimeout(() =>reject(new Error("error")), 1000); setTimeout(() => resolve("ignored"), 2000); >); promise .then( result => alert("Fulfilled: " + result), // не сработает error => alert("Rejected: " + error) // сработает );
Промисификация
Промисификация – это когда берут асинхронную функциональность и делают для неё обёртку, возвращающую промис.
После промисификации использование функциональности зачастую становится гораздо удобнее.
В качестве примера сделаем такую обёртку для запросов при помощи XMLHttpRequest.
Функция httpGet(url) будет возвращать промис, который при успешной загрузке данных с url будет переходить в fulfilled с этими данными, а при ошибке – в rejected с информацией об ошибке:
function httpGet(url) < return new Promise(function(resolve, reject) < var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onload = function() < if (this.status == 200) < resolve(this.response); >else < var error = new Error(this.statusText); error.code = this.status; reject(error); >>; xhr.onerror = function() < reject(new Error("Network Error")); >; xhr.send(); >); >
Как видно, внутри функции объект XMLHttpRequest создаётся и отсылается как обычно, при onload/onerror вызываются, соответственно, resolve (при статусе 200) или reject .
httpGet("/article/promise/user.json") .then( response => alert(`Fulfilled: $`), error => alert(`Rejected: $`) );
Метод fetch
Заметим, что ряд современных браузеров уже поддерживает fetch – новый встроенный метод для AJAX-запросов, призванный заменить XMLHttpRequest. Он гораздо мощнее, чем httpGet . И – да, этот метод использует промисы. Полифил для него доступен на https://github.com/github/fetch.
Цепочки промисов
«Чейнинг» (chaining), то есть возможность строить асинхронные цепочки из промисов – пожалуй, основная причина, из-за которой существуют и активно используются промисы.
Например, мы хотим по очереди:
- Загрузить данные посетителя с сервера (асинхронно).
- Затем отправить запрос о нём на github (асинхронно).
- Когда это будет готово, вывести его github-аватар на экран (асинхронно).
- …И сделать код расширяемым, чтобы цепочку можно было легко продолжить.
Вот код для этого, использующий функцию httpGet , описанную выше:
'use strict'; // сделать запрос httpGet('/article/promise/user.json') // 1. Получить данные о пользователе в JSON и передать дальше .then(response => < console.log(response); let user = JSON.parse(response); return user; >) // 2. Получить информацию с github .then(user => < console.log(user); return httpGet(`https://api.github.com/users/$`); >) // 3. Вывести аватар на 3 секунды (можно с анимацией) .then(githubUser => < console.log(githubUser); githubUser = JSON.parse(githubUser); let img = new Image(); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.appendChild(img); setTimeout(() =>img.remove(), 3000); // (*) >);
Самое главное в этом коде – последовательность вызовов:
httpGet(. ) .then(. ) .then(. ) .then(. )
При чейнинге, то есть последовательных вызовах .then…then…then , в каждый следующий then переходит результат от предыдущего. Вызовы console.log оставлены, чтобы при запуске можно было посмотреть конкретные значения, хотя они здесь и не очень важны.
Если очередной then вернул промис, то далее по цепочке будет передан не сам этот промис, а его результат.
- Функция в первом then возвращает «обычное» значение user . Это значит, что then возвратит промис в состоянии «выполнен» с user в качестве результата. Он станет аргументом в следующем then .
- Функция во втором then возвращает промис (результат нового вызова httpGet ). Когда он будет завершён (может пройти какое-то время), то будет вызван следующий then с его результатом.
- Третий then ничего не возвращает.
Схематично его работу можно изобразить так:
Значком «песочные часы» помечены периоды ожидания, которых всего два: в исходном httpGet и в подвызове далее по цепочке.
Если then возвращает промис, то до его выполнения может пройти некоторое время, оставшаяся часть цепочки будет ждать.
То есть, логика довольно проста:
- В каждом then мы получаем текущий результат работы.
- Можно его обработать синхронно и вернуть результат (например, применить JSON.parse ). Или же, если нужна асинхронная обработка – инициировать её и вернуть промис.
Обратим внимание, что последний then в нашем примере ничего не возвращает. Если мы хотим, чтобы после setTimeout (*) асинхронная цепочка могла быть продолжена, то последний then тоже должен вернуть промис. Это общее правило: если внутри then стартует новый асинхронный процесс, то для того, чтобы оставшаяся часть цепочки выполнилась после его окончания, мы должны вернуть промис.
В данном случае промис должен перейти в состояние «выполнен» после срабатывания setTimeout .
Строку (*) для этого нужно переписать так:
.then(githubUser => < . // вместо setTimeout(() =>img.remove(), 3000); (*) return new Promise((resolve, reject) => < setTimeout(() =>< img.remove(); // после таймаута — вызов resolve, // можно без результата, чтобы управление перешло в следующий then // (или можно передать данные пользователя дальше по цепочке) resolve(); >, 3000); >); >)
Теперь, если к цепочке добавить ещё then , то он будет вызван после окончания setTimeout .
Перехват ошибок
Выше мы рассмотрели «идеальный случай» выполнения, когда ошибок нет.
А что, если github не отвечает? Или JSON.parse бросил синтаксическую ошибку при обработке данных?
Да мало ли, где ошибка…
Правило здесь очень простое.
При возникновении ошибки – она отправляется в ближайший обработчик onRejected .
Такой обработчик нужно поставить через второй аргумент .then(. onRejected) или, что то же самое, через .catch(onRejected) .
Чтобы поймать всевозможные ошибки, которые возникнут при загрузке и обработке данных, добавим catch в конец нашей цепочки:
'use strict'; // в httpGet обратимся к несуществующей странице httpGet('/page-not-exists') .then(response => JSON.parse(response)) .then(user => httpGet(`https://api.github.com/users/$`)) .then(githubUser => < githubUser = JSON.parse(githubUser); let img = new Image(); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.appendChild(img); return new Promise((resolve, reject) => < setTimeout(() =>< img.remove(); resolve(); >, 3000); >); >) .catch(error => < alert(error); // Error: Not Found >);
В примере выше ошибка возникает в первом же httpGet , но catch с тем же успехом поймал бы ошибку во втором httpGet или в JSON.parse .
Принцип очень похож на обычный try..catch : мы делаем асинхронную цепочку из .then , а затем, в том месте кода, где нужно перехватить ошибки, вызываем .catch(onRejected) .
А что после catch ?
Обработчик .catch(onRejected) получает ошибку и должен обработать её.
Есть два варианта развития событий:
- Если ошибка не критичная, то onRejected возвращает значение через return , и управление переходит в ближайший .then(onFulfilled) .
- Если продолжить выполнение с такой ошибкой нельзя, то он делает throw , и тогда ошибка переходит в следующий ближайший .catch(onRejected) .
Это также похоже на обычный try..catch – в блоке catch ошибка либо обрабатывается, и тогда выполнение кода продолжается как обычно, либо он делает throw . Существенное отличие – в том, что промисы асинхронные, поэтому при отсутствии внешнего .catch ошибка не «вываливается» в консоль и не «убивает» скрипт.
Ведь возможно, что новый обработчик .catch будет добавлен в цепочку позже.
Промисы в деталях
Самым основным источником информации по промисам является, разумеется, стандарт.
Чтобы наше понимание промисов было полным, и мы могли с лёгкостью разрешать сложные ситуации, посмотрим внимательнее, что такое промис и как он работает, но уже не в общих словах, а детально, в соответствии со стандартом ECMAScript.
Согласно стандарту, у объекта new Promise(executor) при создании есть четыре внутренних свойства:
- PromiseState – состояние, вначале «pending».
- PromiseResult – результат, при создании значения нет.
- PromiseFulfillReactions – список функций-обработчиков успешного выполнения.
- PromiseRejectReactions – список функций-обработчиков ошибки.
Когда функция-executor вызывает reject или resolve , то PromiseState становится "resolved" или "rejected" , а все функции-обработчики из соответствующего списка перемещаются в специальную системную очередь "PromiseJobs" .
Эта очередь автоматически выполняется, когда интерпретатору «нечего делать». Иначе говоря, все функции-обработчики выполнятся асинхронно, одна за другой, по завершении текущего кода, примерно как setTimeout(. 0) .
Исключение из этого правила – если resolve возвращает другой Promise . Тогда дальнейшее выполнение ожидает его результата (в очередь помещается специальная задача), и функции-обработчики выполняются уже с ним.
Добавляет обработчики в списки один метод: .then(onResolved, onRejected) . Метод .catch(onRejected) – всего лишь сокращённая запись .then(null, onRejected) .
Он делает следующее:
- Если PromiseState == "pending" , то есть промис ещё не выполнен, то обработчики добавляются в соответствующие списки.
- Иначе обработчики сразу помещаются в очередь на выполнение.
Здесь важно, что обработчики можно добавлять в любой момент. Можно до выполнения промиса (они подождут), а можно – после (выполнятся в ближайшее время, через асинхронную очередь).
// Промис выполнится сразу же var promise = new Promise((resolve, reject) => resolve(1)); // PromiseState = "resolved" // PromiseResult = 1 // Добавили обработчик к выполненному промису promise.then(alert); // . он сработает тут же
Разумеется, можно добавлять и много обработчиков на один и тот же промис:
// Промис выполнится сразу же var promise = new Promise((resolve, reject) => resolve(1)); promise.then( function f1(result) < alert(result); // 1 return 'f1'; >) promise.then( function f2(result) < alert(result); // 1 return 'f2'; >)
Вид объекта promise после этого:
На этой иллюстрации можно увидеть добавленные нами обработчики f1 , f2 , а также – автоматически добавленные обработчики ошибок "Thrower" .
Дело в том, что .then , если один из обработчиков не указан, добавляет его «от себя», следующим образом:
- Для успешного выполнения – функция Identity , которая выглядит как arg => arg , то есть возвращает аргумент без изменений.
- Для ошибки – функция Thrower , которая выглядит как arg => throw arg , то есть генерирует ошибку.
Это, по сути дела, формальность, но без неё некоторые особенности поведения промисов могут «не сойтись» в общую логику, поэтому мы упоминаем о ней здесь.
Обратим внимание, в этом примере намеренно не используется чейнинг. То есть, обработчики добавляются именно на один и тот же промис.
Поэтому оба alert выдадут одно значение 1 .
Все функции из списка обработчиков вызываются с результатом промиса, одна за другой. Никакой передачи результатов между обработчиками в рамках одного промиса нет, а сам результат промиса ( PromiseResult ) после установки не меняется.
Поэтому, чтобы продолжить работу с результатом, используется чейнинг.
Для того, чтобы результат обработчика передать следующей функции, .then создаёт новый промис и возвращает его.
В примере выше создаётся два таких промиса (т.к. два вызова .then ), каждый из которых даёт свою ветку выполнения:
Изначально эти новые промисы – «пустые», они ждут. Когда в будущем выполнятся обработчики f1, f2 , то их результат будет передан в новые промисы по стандартному принципу:
- Если вернётся обычное значение (не промис), новый промис перейдёт в "resolved" с ним.
- Если был throw , то новый промис перейдёт в состояние "rejected" с ошибкой.
- Если вернётся промис, то используем его результат (он может быть как resolved , так и rejected ).
Дальше выполнятся уже обработчики на новом промисе, и так далее.
Чтобы лучше понять происходящее, посмотрим на цепочку, которая получается в процессе написания кода для показа github-аватара.
Первый промис и обработка его результата:
httpGet('/article/promise/user.json') .then(JSON.parse)
Если промис завершился через resolve , то результат – в JSON.parse , если reject – то в Thrower.
Как было сказано выше, Thrower – это стандартная внутренняя функция, которая автоматически используется, если второй обработчик не указан.
Можно считать, что второй обработчик выглядит так:
httpGet('/article/promise/user.json') .then(JSON.parse, err => throw err)
Заметим, что когда обработчик в промисах делает throw – в данном случае, при ошибке запроса, то такая ошибка не «валит» скрипт и не выводится в консоли. Она просто будет передана в ближайший следующий обработчик onRejected .
Добавим в код ещё строку:
httpGet('/article/promise/user.json') .then(JSON.parse) .then(user => httpGet(`https://api.github.com/users/$`))
Цепочка «выросла вниз»:
Функция JSON.parse либо возвращает объект с данными, либо генерирует ошибку (что расценивается как reject ).
Если всё хорошо, то then(user => httpGet(…)) вернёт новый промис, на который стоят уже два обработчика:
httpGet('/article/promise/user.json') .then(JSON.parse) .then(user => httpGet(`https://api.github.com/users/$`)) .then( JSON.parse, function avatarError(error) < if (error.code == 404) < return ; > else < throw error; >> >)
Наконец-то хоть какая-то обработка ошибок!
Обработчик avatarError перехватит ошибки, которые были ранее. Функция httpGet при генерации ошибки записывает её HTTP-код в свойство error.code , так что мы легко можем понять – что это:
- Если страница на Github не найдена – можно продолжить выполнение, используя «аватар по умолчанию»
- Иначе – пробрасываем ошибку дальше.
Итого, после добавления оставшейся части цепочки, картина получается следующей:
'use strict'; httpGet('/article/promise/userNoGithub.json') .then(JSON.parse) .then(user => httpGet(`https://api.github.com/users/$`)) .then( JSON.parse, function githubError(error) < if (error.code == 404) < return ; > else < throw error; >> ) .then(function showAvatar(githubUser) < let img = new Image(); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.appendChild(img); setTimeout(() =>img.remove(), 3000); >) .catch(function genericError(error) < alert(error); // Error: Not Found >);
В конце срабатывает общий обработчик genericError , который перехватывает любые ошибки. В данном случае ошибки, которые в него попадут, уже носят критический характер, что-то серьёзно не так. Чтобы посетитель не удивился отсутствию информации, мы показываем ему сообщение об этом.
Можно и как-то иначе вывести уведомление о проблеме, главное – не забыть обработать ошибки в конце. Если последнего catch не будет, а цепочка завершится с ошибкой, то посетитель об этом не узнает.
В консоли тоже ничего не будет, так как ошибка остаётся «внутри» промиса, ожидая добавления следующего обработчика onRejected , которому будет передана.
Итак, мы рассмотрели основные приёмы использования промисов. Далее – посмотрим некоторые полезные вспомогательные методы.
Параллельное выполнение
Что, если мы хотим осуществить несколько асинхронных процессов одновременно и обработать их результат?
В классе Promise есть следующие статические методы.
Promise.all(iterable)
Вызов Promise.all(iterable) получает массив (или другой итерируемый объект) промисов и возвращает промис, который ждёт, пока все переданные промисы завершатся, и переходит в состояние «выполнено» с массивом их результатов.
Promise.all([ httpGet('/article/promise/user.json'), httpGet('/article/promise/guest.json') ]).then(results => < alert(results); >);
Допустим, у нас есть массив с URL.
let urls = [ '/article/promise/user.json', '/article/promise/guest.json' ];
Чтобы загрузить их параллельно, нужно:
- Создать для каждого URL соответствующий промис.
- Обернуть массив таких промисов в Promise.all .
'use strict'; let urls = [ '/article/promise/user.json', '/article/promise/guest.json' ]; Promise.all( urls.map(httpGet) ) .then(results => < alert(results); >);
Заметим, что если какой-то из промисов завершился с ошибкой, то результатом Promise.all будет эта ошибка. При этом остальные промисы игнорируются.
Promise.all([ httpGet('/article/promise/user.json'), httpGet('/article/promise/guest.json'), httpGet('/article/promise/no-such-page.json') // (нет такой страницы) ]).then( result => alert("не сработает"), error => alert("Ошибка: " + error.message) // Ошибка: Not Found )
Promise.race(iterable)
Вызов Promise.race , как и Promise.all , получает итерируемый объект с промисами, которые нужно выполнить, и возвращает новый промис.
Но, в отличие от Promise.all , результатом будет только первый успешно выполнившийся промис из списка. Остальные игнорируются.
Promise.race([ httpGet('/article/promise/user.json'), httpGet('/article/promise/guest.json') ]).then(firstResult => < firstResult = JSON.parse(firstResult); alert( firstResult.name ); // iliakan или guest, смотря что загрузится раньше >);
Promise.resolve(value)
Вызов Promise.resolve(value) создаёт успешно выполнившийся промис с результатом value .
Он аналогичен конструкции:
new Promise((resolve) => resolve(value))
Promise.resolve используют, когда хотят построить асинхронную цепочку, и начальный результат уже есть.
Promise.resolve(window.location) // начать с этого значения .then(httpGet) // вызвать для него httpGet .then(alert) // и вывести результат
Promise.reject(error)
Аналогично Promise.reject(error) создаёт уже выполнившийся промис, но не с успешным результатом, а с ошибкой error .
Promise.reject(new Error(". ")) .catch(alert) // Error: .
Метод Promise.reject используется очень редко, гораздо реже чем resolve , потому что ошибка возникает обычно не в начале цепочки, а в процессе её выполнения.
Итого
- Промис – это специальный объект, который хранит своё состояние, текущий результат (если есть) и колбэки.
- При создании new Promise((resolve, reject) => . ) автоматически запускается функция-аргумент, которая должна вызвать resolve(result) при успешном выполнении и reject(error) – при ошибке.
- Аргумент resolve/reject (только первый, остальные игнорируются) передаётся обработчикам на этом промисе.
- Обработчики назначаются вызовом .then/catch .
- Для передачи результата от одного обработчика к другому используется чейнинг.
У промисов есть некоторые ограничения. В частности, стандарт не предусматривает какой-то метод для «отмены» промиса, хотя в ряде ситуаций (http-запросы) это было бы довольно удобно. Возможно, он появится в следующей версии стандарта JavaScript.
В современной JavaScript-разработке сложные цепочки с промисами используются редко, так как они куда проще описываются при помощи генераторов с библиотекой co , которые рассмотрены в соответствующей главе. Можно сказать, что промисы лежат в основе более продвинутых способов асинхронной разработки.
Задачи
Промисифицировать setTimeout
Напишите функцию delay(ms) , которая возвращает промис, переходящий в состояние "resolved" через ms миллисекунд.
delay(1000) .then(() => alert("Hello!"))
Такая функция полезна для использования в других промис-цепочках.
Вот такой вызов:
return new Promise((resolve, reject) => < setTimeout(() =>< doSomeThing(); resolve(); >, ms) >);
Станет возможным переписать так:
return delay(ms).then(doSomething);
Promise
Объект Promise используется для отложенных и асинхронных вычислений.
Интерактивный пример
Синтаксис
new Promise(executor); new Promise(function(resolve, reject) . >);
Параметры
Объект функции с двумя аргументами resolve и reject . Функция executor получает оба аргумента и выполняется сразу, ещё до того как конструктор вернёт созданный объект. Первый аргумент ( resolve ) вызывает успешное исполнение промиса, второй ( reject ) отклоняет его. Обычно функция executor описывает выполнение какой-то асинхронной работы, по завершении которой необходимо вызвать функцию resolve или reject . Обратите внимание, что возвращаемое значение функции executor игнорируется.
Описание
Интерфейс Promise (промис) представляет собой обёртку для значения, неизвестного на момент создания промиса. Он позволяет обрабатывать результаты асинхронных операций так, как если бы они были синхронными: вместо конечного результата асинхронного метода возвращается своего рода обещание (дословный перевод слова "промис") получить результат в некоторый момент в будущем.
Promise может находиться в трёх состояниях:
- ожидание (pending): начальное состояние, не исполнен и не отклонён.
- исполнено (fulfilled): операция завершена успешно.
- отклонено (rejected): операция завершена с ошибкой.
При создании промис находится в ожидании (pending), а затем может стать исполненным (fulfilled), вернув полученный результат (значение), или отклонённым (rejected), вернув причину отказа. В любом из этих случаев вызывается обработчик, прикреплённый к промису методом then . (Если в момент назначения обработчика промис уже исполнен или отклонён, обработчик всё равно будет вызван, т.е. асинхронное исполнение промиса и назначение обработчика не будет происходить в «состоянии гонки», как, например, в случае с событиями в DOM.)
Так как методы Promise.prototype.then() и Promise.prototype.catch() сами возвращают промис, их можно вызывать цепочкой, создавая соединения.
Примечание: говорят, что промис находится в состоянии завершён (settled) когда он или исполнен или отклонён, т.е. в любом состоянии, кроме ожидания (это лишь форма речи, не являющаяся настоящим состоянием промиса). Также можно встретить термин исполнен (resolved) — это значит что промис завершён или "заблокирован" в ожидании завершения другого промиса. В статье состояния и fates приводится более подробное описание терминологии.
Свойства
Значение свойства всегда равно 1 (количество аргументов конструктора).
Представляет прототип для конструктора Promise .
Методы
Ожидает исполнения всех промисов или отклонения любого из них.
Возвращает промис, который исполнится после исполнения всех промисов в iterable . В случае, если любой из промисов будет отклонён, Promise.all будет также отклонён.
Ожидает завершения всех полученных промисов (как исполнения так и отклонения).
Возвращает промис, который исполняется когда все полученные промисы завершены (исполнены или отклонены), содержащий массив результатов исполнения полученных промисов.
Ожидает исполнения или отклонения любого из полученных промисов.
Возвращает промис, который будет исполнен или отклонён с результатом исполнения первого исполненного или отклонённого промиса из iterable .
Возвращает промис, отклонённый из-за reason .
Возвращает промис, исполненный с результатом value .
Создание промиса
Объект Promise создаётся при помощи ключевого слова new и своего конструктора. Конструктор Promise принимает в качестве аргумента функцию, называемую "исполнитель" (executor function). Эта функция должна принимать две функции-колбэка в качестве параметров. Первый из них ( resolve ) вызывается, когда асинхронная операция завершилась успешно и вернула результат своего исполнения в виде значения. Второй колбэк ( reject ) вызывается, когда операция не удалась, и возвращает значение, указывающее на причину неудачи, чаще всего объект ошибки.
const myFirstPromise = new Promise((resolve, reject) => // выполняется асинхронная операция, которая в итоге вызовет: // // resolve(someValue); // успешное завершение // или // reject("failure reason"); // неудача >);
Чтобы снабдить функцию функциональностью промисов, нужно просто вернуть в ней объект Promise :
function myAsyncFunction(url) return new Promise((resolve, reject) => const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText); xhr.send(); >); >
Примеры
Простой пример
js
let myFirstPromise = new Promise((resolve, reject) => // Мы вызываем resolve(. ), когда асинхронная операция завершилась успешно, и reject(. ), когда она не удалась. // В этом примере мы используем setTimeout(. ), чтобы симулировать асинхронный код. // В реальности вы, скорее всего, будете использовать XHR, HTML5 API или что-то подобное. setTimeout(function () resolve("Success!"); // Ура! Всё прошло хорошо! >, 250); >); myFirstPromise.then((successMessage) => // successMessage - это что угодно, что мы передали в функцию resolve(. ) выше. // Это необязательно строка, но если это всего лишь сообщение об успешном завершении, это наверняка будет она. console.log("Ура! " + successMessage); >);Продвинутый пример
html
button id="btn">Создать Promise!button> div id="log">div>исполнение промиса протоколируется при помощи продолжения p1.then . Это показывает как синхронная часть метода отвязана от асинхронного завершения промиса.
var promiseCount = 0; function testPromise() var thisPromiseCount = ++promiseCount; var log = document.getElementById('log'); log.insertAdjacentHTML('beforeend', thisPromiseCount + ') Запуск (запуск синхронного кода) '); // Создаём промис, возвращающее 'result' (по истечении 3-х секунд) var p1 = new Promise( // Функция разрешения позволяет завершить успешно или // отклонить промис function(resolve, reject) log.insertAdjacentHTML('beforeend', thisPromiseCount + ') Запуск промиса (запуск асинхронного кода) '); // Это всего лишь пример асинхронности window.setTimeout( function() // Промис исполнен! resolve(thisPromiseCount) >, Math.random() * 2000 + 1000); >); // Указываем, что сделать с исполненным промисом p1.then( // Записываем в протокол function(val) log.insertAdjacentHTML('beforeend', val + ') Промис исполнен (асинхронный код завершён) '); >); log.insertAdjacentHTML('beforeend', thisPromiseCount + ') Промис создан (синхронный код завершён) '); >if ("Promise" in window) btn = document.getElementById("btn"); btn.addEventListener("click", testPromise); > else log = document.getElementById("log"); log.innerHTML = "Live example not available as your browser doesn't support the Promise interface."; >if ("Promise" in window) let btn = document.getElementById("btn"); btn.addEventListener("click", testPromise); > else log = document.getElementById("log"); log.innerHTML = "Демонстрация невозможна, поскольку ваш браузер не поддерживает интерфейсPromise
; >."
Данный пример запускается при нажатии на кнопку. Для этого вам необходим браузер, поддерживающий Promise . При последовательных нажатиях на кнопку с коротким интервалом, вы можете увидеть как различные промиса будут исполнены один за другим.
Загрузка изображения при помощи XHR
Другой простой пример использования Promise и XMLHttpRequest для загрузки изображения доступен в репозитории MDNpromise-test на GitHub. Вы также можете посмотреть его в действии. Каждый шаг прокомментирован и вы можете подробно исследовать Promise и XHR.
Спецификации
Specification |
---|
ECMAScript Language Specification # sec-promise-objects |
Совместимость с браузерами
BCD tables only load in the browser
Смотрите также
- Спецификация Promises/A+
- Jake Archibald: JavaScript Promises: There and Back Again
- Domenic Denicola: Callbacks, Promises, and Coroutines – Asynchronous Programming Pattern in JavaScript
- Matt Greer: JavaScript Promises . In Wicked Detail
- Forbes Lindesay: promisejs.org
- Nolan Lawson: We have a problem with promises — Common mistakes with promises
- Promise polyfill
- Udacity: JavaScript Promises
Found a content problem with this page?
- Edit the page on GitHub.
- Report the content issue.
- View the source on GitHub.
This page was last modified on 7 авг. 2023 г. by MDN contributors.
Your blueprint for a better internet.
JavaScript: Что такое Промисы / Promise
Разберёмся как создавать промисы и работать с ними в JavaScript. Рассмотрим цепочки промисов, обработку ошибок и некоторые из новых методов промисов, добавленных в язык.
Что такое Промисы/Promise в JavaScript
В JavaScript некоторые операции асинхронны. Это означает, что результат или значение, которые они производят, не доступны сразу после завершения операции.
Промис/ Promise — специальный объект JavaScript предоставляющий конечный результат такой асинхронной операции. Он действует как прокси для результата операции.
Старые недобрые времена: функции обратного вызова
До того как появились JavaScript промисы, предпочтительным способом работы с асинхронной операцией было использование обратного вызова. Обратный вызов или Callback — это функция, запускающаяся, когда готов результат асинхронной операции. Например:
setTimeout(function()
console.log('Hello, World!');
>, 1000);
setTimeout — асинхронная функция запускающая любую переданную ей функцию обратного вызова через указанное количество миллисекунд. В данном случае Hello, World! выведется в консоль через одну секунду.
Теперь представьте, что мы хотим выводить в консоль сообщение каждую секунду в течение пяти секунд. Это будет выглядеть так:
setTimeout(function()
console.log(1);
setTimeout(function()
console.log(2);
setTimeout(function()
console.log(3);
setTimeout(function()
console.log(4);
setTimeout(function()
console.log(5);
>, 1000);
>, 1000);
>, 1000);
>, 1000);
>, 1000);
Асинхронный JavaScript использующий таким образом несколько вложенных обратных вызовов, подвержен ошибкам и сложен в обслуживании. Его часто называют адом обратных вызовов.
Это надуманный пример, но он служит для иллюстрации сути. В реальном сценарии мы могли бы сделать вызов Ajax, обновить DOM с результатом, а затем дождаться завершения анимации. Или, сервер может получить данные от клиента, проверить их, обновить базу данных, записать лог и, наконец, отправить ответ. В обоих случаях нам необходимо обрабатывать любые возникающие ошибки.
Использование вложенных обратных вызовов для выполнения таких задач было бы болезненным занятием. К счастью, промисы предоставляют гораздо более чистый синтаксис позволяющий связывать асинхронные команды так, чтобы они выполнялись одна за другой.
Как создать объект Promise в JavaScript
Базовый синтаксис создания промиса следующий:
const promise = new Promise((resolve, reject) =>
//здесь идёт асинхронный код
>);
Начинаем с создания экземпляра нового объекта Promise с помощью конструктора Promise() и передачи ему функции обратного вызова. Обратный вызов принимает два аргумента, resolve и reject , являющиеся функциями. Весь асинхронный код находится внутри этого вызова.
Если всё выполняется успешно, промис fulfilled и выполняется вызов resolve . В случае ошибки промис отклоняется вызовом reject . Мы можем передавать значение обоим параметрам, которые затем будут доступны в коде.
Для демонстрации как это работает на практике, рассмотрим следующий код. Он делает асинхронный запрос к веб-сервису возвращающему случайные шутки в формате JSON:
const promise = new Promise((resolve, reject) =>
const request = new XMLHttpRequest();
request.open('GET', 'https://icanhazdadjoke.com/');
request.setRequestHeader('Accept', 'application/json');
request.onload = () =>
if (request.status === 200)
resolve(request.response); // у нас есть данные, так что resolve Promise
> else
reject(Error(request.statusText)); // статус не 200 OK, так что reject
>
>;
request.onerror = () =>
reject(Error('Error fetching data.')); // произошла ошибка, reject Promise
>;
request.send(); // посылаем запрос
>);
https://icanhazdadjoke.com/ ограничили доступ для некоторых стран, поэтому примеры могут не работать. Попробуйте прокси или VPN.
Конструктор Promise
Создаём объект promise с использованием конструктора Promise . Конструктор используется для оборачивания функции или API, которые не поддерживают промисы, например объект XMLHttpRequest . Обратный вызов, переданный конструктору промиса содержит асинхронный код, используемый для получения данных из удалённой службы. Обратите внимание, что используется стрелочная функция. Внутри обратного вызова создаём Ajax запрос к https://icanhazdadjoke.com/ , который возвращает случайную шутку в формате JSON.
Когда от удалённого сервера получен успешный ответ, он передаётся методу resolve . В случае возникновения какой-либо ошибки — либо на сервере, либо на сетевом уровне — вызывается reject с объектом Error .
Метод then
Когда мы создаём объект promise , то получаем прокси для данных, которые будут доступны в будущем. В нашем случае мы ожидаем, что некие данные будут возвращены удалённой службой. Итак, как мы узнаем, когда данные будут доступны? Для этого используется функция Promise.then() :
const promise = new Promise((resolve, reject) => . >);
promise.then((data) =>
console.log('Got data! Promise fulfilled.');
document.body.textContent = JSON.parse(data).joke;
>, (error) =>
console.error('Promise rejected.');
console.error(error.message);
>);
Эта функция может принимать два аргумента: обратный вызов при успешном выполнении resolve и обратный вызов при отказе reject . Эти обратные вызовы выполняются когда промис завершён (то есть либо выполнен resolve , либо отклонён reject ). Если промис был выполнен, обратный вызов resolve будет выполнен с фактическими данными полученными из удалённого сервиса. Если промис был отклонён, то будет вызван обратный вызов reject . Всё, что мы передали для отказа, будет передано в качестве аргумента этому обратному вызову.
Можете попробовать этот код в демо на CodePen. Чтобы посмотреть новую случайную шутку, нажмите кнопку RERUN в правом нижнем углу окна. Может не работать без прокси или VPN из некоторых стран
Какие состояния бывают у промиса в JavaScript
В приведённом выше коде мы увидели, что можем изменить состояние промиса, вызвав методы resolve или reject . Прежде чем пойдём дальше, давайте рассмотрим жизненный цикл промиса.
Промис может быть в одном из этих состояний:
Промис начинает жизненный цикл с состояний pending . Это означает, что он не был ни fulfilled , ни rejected . Если действие связанное с промисом успешно выполнено (в нашем случае удалённый вызов API) и вызывается метод resolve , считается, что промис fulfilled — выполнен. Наконец, считается, что промис settled — выполнен, если он находится в состоянии fulfilled или rejected , но не pending .
Как только промис отклоняется rejected или выполняется fulfilled , этот статус навсегда ассоциируется с ним. Это означает, что промис может быть успешным или проваленным только один раз. Если промис уже был fulfilled , а позже мы присоединяем к нему .then() с двумя обратными вызовами, успешный обратный вызов будет корректно вызван. Итак, в мире промисов нам не интересно, когда промис выполнился. Нас интересует только окончательный результат промиса.
Но разве мы не должны использовать Fetch API
Сейчас мы можем спросить, почему не используем Fetch API для получения данных с удалённого сервера, и ответ заключается в том, что, вероятно, нам следует это сделать.
В отличие от объекта XMLHttpRequest , Fetch API основан на промисах, это означает, что мы можем переписать наш код следующим образом (минус обработка ошибок):
fetch('https://icanhazdadjoke.com',
headers: 'Accept': 'application/json' >
>)
.then(res => res.json())
.then(json => console.log(json.joke));
Причина использования XMLHttpRequest заключалась в том, чтобы лучше понять, что происходит под капотом.
Цепочки Промисов
Иногда бывает нужно связать несколько асинхронных задач в определённом порядке. Это называется цепочка промисов. Давайте вернёмся к нашему примеру с setTimeout , чтобы получить общее представление о том, как работает цепочка промисов.
Мы могли бы начать с создания нового объекта промиса, как мы делали это ранее:
const promise = new Promise((resolve, reject) =>
setTimeout(() => resolve() >, 1000)
>);
promise.then(() =>
console.log(1);
>);
Как и ожидалось, промис завершается через одну секунду и выводит в консоль 1 .
Для продолжения цепочки необходимо вернуть второй промис после вывода в консоль и передать его второму then :
const promise = new Promise((resolve, reject) =>
setTimeout(() => resolve() >, 1000)
>);
promise.then(() =>
console.log(1);
return new Promise((resolve, reject) =>
setTimeout(() => resolve() >, 1000)
>);
>).then(() =>
console.log(2);
>);
И хотя это работает, оно уже начинает становиться громоздким. Давайте создадим функцию, возвращающую новый промис, завершающийся по истечении некого периода времени:
function sleep(ms)
return new Promise(resolve => setTimeout(resolve, ms));
>
Теперь мы можем использовать этот фрагмент для сглаживания вложенного кода:
sleep(1000)
.then(() =>
console.log(1);
return sleep(1000);
>).then(() =>
console.log(2);
return sleep(1000);
>).then(() =>
console.log(3);
return sleep(1000);
>)
.
А поскольку сам метод then возвращает объект промис, и мы не передаём никаких значений от одной асинхронной операции к другой, это позволяет нам ещё больше упростить ситуацию:
sleep(1000)
.then(() => console.log(1))
.then(() => sleep(1000))
.then(() => console.log(2))
.then(() => sleep(1000))
.then(() => console.log(3))
.
Этот код намного элегантнее, чем первоначальный вариант.
Обратите внимание, что если вы хотите больше узнать о реализации sleep() в JavaScript, возможно вам будет интересна статья: JavaScript: Delay, Sleep, Pause, & Wait
Передача данных по цепочке промисов
Когда есть несколько асинхронных операций, которые нужно выполнить. Вероятно, что мы захотим передать результат одного асинхронного вызова следующему, а затем заблокировать в цепочке промисов, чтобы что-нибудь сделать с этими данными.
Например, нам может понадобиться получить список участников репозитория GitHub, а затем использовать эту информацию для получения имени первого участника:
fetch('https://api.github.com/repos/eslint/eslint/contributors')
.then(res => res.json())
.then(json =>
const firstContributor = json[0].login;
return fetch(`https://api.github.com/users/$firstContributor>`)
>)
.then(res => res.json())
.then(json => console.log(`The first contributor to ESLint was $json.name>`));
// The first contributor to ESLint was Nicholas C. Zakas
Как видите, возвращая промис из второго вызов fetch, ответ сервера res доступен в следующем блоке .then .
Обработка ошибок промиса
Как уже упоминалась функция then принимает две функции обратного вызова в качестве аргументов. Вторая функция будет вызвана, если промис был отклонён:
promise.then((data) =>
console.log('Got data! Promise fulfilled.');
.
>, (error) =>
console.error('Promise rejected.');
console.error(error.message);
>);
При работе с цепочками промисов указание отдельного обработчика ошибок для каждого промиса может стать довольно утомительным занятием. К счастью, есть лучший способ…
Метод catch
Можно использовать метод catch , который может обрабатывать ошибки вместо нас. Когда где-то в цепочке промис отклоняется, управление переходит к ближайшему обработчику отклонений. Это удобно, так как мы можем добавить catch в конце цепочки и заставить её обрабатывать любые возникающие ошибки.
Возьмём в качестве примера предыдущий код:
fetch('https://api.github.com/repos/eslint/eslint/contributors')
.then(res => res.json())
.then(json =>
const firstContributor = json[0].login;
return fetch(`https://api.github.com/users/$firstContributor>`)
>)
.then(res => res.jsn())
.then(json => console.log(`The top contributor to ESLint wass $json.name>`))
.catch(error => console.log(error));
Обратите внимание, что в дополнение к добавленному обработчику ошибок в конце блока неправильно написан res.json() , в седьмой строке (она выделена) написано res.jsn() .
При запуске кода получаем следующее сообщение:
TypeError: res.jsn is not a function
http://0.0.0.0:8000/index.js:7
promise callback* http://0.0.0.0:8000/index.js:7
index.js:9:27
Файл с которым мы работаем называется index.js . Строка 7 содержит ошибку, а строка 9 — это блок catch , обнаруживший ошибку.
Метод finally
Метод Promise.finally запускается, когда промис выполнен, то есть либо resolved , либо rejected . Как и catch , он помогает предотвратить дублирование кода и полезен для выполнения задач очистки, таких как закрытие соединения с базой данных или удаления спиннера загрузки из пользовательского UI.
function getFirstContributor(org, repo)
showLoadingSpinner();
fetch(`https://api.github.com/repos/$org>/$repo>/contributors`)
.then(res => res.json())
.then(json =>
const firstContributor = json[0].login;
return fetch(`https://api.github.com/users/$firstContributor>`)
>)
.then(res => res.json())
.then(json => console.log(`The first contributor to $repo> was $json.name>`))
.catch(error => console.log(error))
.finally(() => hideLoadingSpinner());
>;
getFirstContributor('facebook', 'react');
Он не получает никаких аргументов и возвращает промис, так что можно добавить ещё then , catch и finally вызовов с его возвращаемым значением.
Дополнительные методы промиса
К этому моменту у нас есть хорошее базово понимание работы с промисами в JavaScript, но прежде чем мы закончим, нужно знать о различных служебных методах промиса.
Promise.all()
В отличие от предыдущего примера, где нам нужно было завершить первый вызов Ajax, прежде чем мы сможем сделать второй, иногда у нас будет множество асинхронных операций, которые вообще не зависят друг от друга. Именно тогда появляется Promise.all .
Этот метод принимает массив промисов и ожидает, пока все промисы не будут разрешены или какие-либо будут отклонены. Если все промисы успешно разрешаются, all завершается с массивом значений завершённых отдельных промисов:
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 0)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
.then(values => console.log(values))
.catch(err => console.error(err));
Этот код выведет в консоль [1, 2, 3] через три секунды.
Однако если какой-либо из промисов будет отклонён, метод all будет отклонён со значением этого промиса и не будет принимать никаких других промисов.
Promise.allSettled()
В отличие от all , Promise.allSettled() будет ждать завершения каждого переданного промиса. Он не останавливает выполнение в случае отклонения промиса:
Promise.allSettled([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 0)),
new Promise((resolve, reject) => setTimeout(() => reject(2), 1500)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
.then(values => console.log(values))
.catch(err => console.error(err));
Этот код вернёт список статусов и значений (если промис будет выполнен) или причин (если промис будет отклонён):
[
status: "fulfilled", value: 1 >,
status: "rejected", reason: 2 >,
status: "fulfilled", value: 3 >,
]
Promise.any()
Promise.any возвращает значение первого выполненного промиса. Если какие-либо промисы были отклонены, они игнорируются:
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(1), 0)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
.then(values => console.log(values))
.catch(err => console.error(err));
Этот код выведет в консоль 2 через полторы секунды.
Promise.race()
Promise.race также получает массив промисов и (как и другие вышеперечисленные методы) возвращает новый промис. Как только один из полученных промисов выполняется или отклоняется, race сама либо выполняет, либо отклоняет значение или причину из завершённого промиса. Проще говоря, race возвращает результат первого завершившегося промиса. Если промис завершился удачно, то возвращается результат. Если промис был отклонён, то возвращается причина отклонения:
Promise.race([
new Promise((resolve, reject) => setTimeout(() => reject('Rejected with 1'), 0)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
.then(values => console.log(values))
.catch(err => console.error(err));
Код выведет в консоль Rejected with 1 , так как первый промис в массиве немедленно отклоняется, и отклонение перехватывается блоком catch .
Мы могли бы изменить код так (в первом промисе заменим reject на resolve ):
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve('Resolved with 1'), 0)),
new Promise((resolve, reject) => setTimeout(() => resolve(2), 1500)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
])
.then(values => console.log(values))
.catch(err => console.error(err));
В консоль выведется Resolved with 1 .
В обоих случаях два других промиса игнорируются.
Примеры JavaScript промисов
Давайте посмотрим на код в действии. Вот две демонстрации объединяющие несколько концепций, рассмотренных нами на протяжении всей статьи.
Поиск автора репозитория GitHub
Первая демонстрация позволяет пользователю ввести URL-адрес репозитория GitHub. Затем выполняется Ajax-запрос для получения списка первых 30 участников этого репозитория. Когда этот запрос завершится, выполниться второй запрос, получающий имя исходного участника и отобразит его на странице. Для второго запроса используются данные возвращённые первым запросом.
Чтобы продемонстрировать использование finally , я добавил к сетевому запросу задержку, в течении которой показывается спиннер загрузки. Он удаляется, когда запрос завершён.
Определение у какого репозитория GitHub больше звёзд
В этом примере пользователь ввод два URL-адреса GitHub репозиториев. Затем скрипт использует Promise.all для выполнения двух параллельных запросов получения базовой информации об этих репозиториях. Мы можем использовать all , так как оба сетевых запроса независимы друг от друга. В отличие от предыдущего примера, результат одного запроса не зависит от другого.
После выполнения запросов, скрипт выведет в каком репозитории больше звёзд, а в каком меньше.
Промисы, обратные вызовы или async/await: что нужно использовать
До сих пор мы рассматривали обратные вызовы и промисы, но также стоит упомянуть более новый async/await синтаксис. Хотя по сути это всего лишь синтаксический сахар поверх промисов, во многих случаях он может упростить чтение и понимание кода основанного на промисах.
Например, так мы могли бы переписать предыдущий код:
async function getFirstContributor(org, repo)
showLoadingSpinner();
try
const res1 = await fetch(`https://apiy.github.com/repos/$org>/$repo>/contributors`);
const contributors = await res1.json();
const firstContributor = contributors[0].login;
const res2 = await fetch(`https://api.github.com/users/$firstContributor>`)
const details = await res2.json();
console.log(`The first contributor to $repo> was $details.name>`);
> catch (error)
console.error(error)
> finally
hideLoadingSpinner();
>
>
getFirstContributor('facebook', 'react');
Как видите мы используем синтаксис try. catch для обработки ошибок, и мы можем сделать небольшую чистку внутри блока finally .
Думаю, что приведённый выше код немного легче разобрать, чем версию на промисах. Тем не менее я посоветовал ознакомиться с синтаксисом async/await и посмотреть, что лучше подходит для вас. Хорошим стартом будет статья JavaScript: Управление потоком, в которой рассматриваются многие преимущества и недостатки соответствующих методов.
Также нужно быть осторожным при смешивании обоих стилей, так как обработка ошибок иногда может вести себя неожиданно. По сути, отклонённые промисы это не то же самое, что асинхронные ошибки, и это может привести к проблемам, как показано в этой статье.
Заключение
В этой статье мы рассмотрели, как создавать JavaScript промисы и как с ними работать. Мы научились создавать цепочки промисов и передавать данные из одной асинхронной операции в другую. Также рассмотрели обработку ошибок и различные служебные методы промисов.
Как упоминалось выше, отличным следующим шагом было бы изучение async/await и углубление понимания управления потоком внутри JavaScript программы.