Что такое promise js
Промис (promise) — это объект, представляющий результат успешного или неудачного завершения асинхронной операции. Асинхронная операция, упрощенно говоря, это некоторое действие, выполняется независимо от окружающего ее кода, в котором она вызывается, не блокирует выполнение вызываемого кода.
Промис может находиться в одном из следующих состояний:
- pending (состояние ожидания): начальное состояние, промис создан, но выполнение еще не завершено
- fulfilled (успешно завершено): действие, которое представляет промис, успешно завершено
- rejected (завершено с ошибкой): при выполнении действия, которое представляет промис, произошла ошибка
Для создания промиса применяется конструктор типа Promise :
new Promise(executor)
В качестве параметра конструктор принимает функцию, которая выполняется при создании промиса. Обычно эта функция представляет асинхронные операции, которые занимают продолжительное время. Например, определим простейший промис:
const myPromise = new Promise(function()< console.log("Выполнение асинхронной операции"); >);
Здесь функция просто выводит на консоль сообщение. Соответственно при выполнении этого кода мы увидим на консоли сообщение «Выполнение асинхронной операции» .
При создании промиса, когда его функция еще не начала выполняться, промис переходит в состояние «pending», то есть ожидает выполнения.
Для эмуляции асинхронности определим несколько промисов:
const myPromise3000 = new Promise(function()< console.log("[myPromise3000] Выполнение асинхронной операции"); setTimeout(()=>console.log("[myPromise3000] Завершение асинхронной операции"), 3000); >); const myPromise1000 = new Promise(function()< console.log("[myPromise1000] Выполнение асинхронной операции"); setTimeout(()=>console.log("[myPromise1000] Завершение асинхронной операции"), 1000); >); const myPromise2000 = new Promise(function()< console.log("[myPromise2000] Выполнение асинхронной операции"); setTimeout(()=>console.log("[myPromise2000] Завершение асинхронной операции"), 2000); >);
Здесь определены три однотипных промиса. Чтобы каждый из них не выполнялся сразу, они используют функцию setTimeout с задержкой в несколько секунд. Для разных промисов длительность задержки различается. И в данном случае мы получим следующий консольный вывод:
[myPromise3000] Выполнение асинхронной операции [myPromise1000] Выполнение асинхронной операции [myPromise2000] Выполнение асинхронной операции [myPromise1000] Завершение асинхронной операции [myPromise2000] Завершение асинхронной операции [myPromise3000] Завершение асинхронной операции
Здесь мы видим, что первым начал выполняться промис myPromise3000, однако он же завершился последним, так как для него установлено наибольшее время задержки — 3 секунды. Однако его задержка не помешала выполнению остальных промисов.
resolve и reject
Как правило, функция, которая передается в конструктор Promise, принимает два параметра:
const myPromise = new Promise(function(resolve, reject)< console.log("Выполнение асинхронной операции"); >);
Оба этих параметра — resolve и reject также представляют функции. И каждая из этих функций принимает параметр любого типа.
Первый параметр — функция resolve вызывается в случае успешного выполнения. Мы можем в нее передать значение, которое мы можем получить в результате успешного выполнения.
Второй параметр — функция reject вызывается, если выполнение операции завершилось с ошибкой. Мы можем в нее передать значение, которое представит некоторую информацию об ошибке.
Успешное выполнение промиса
Итак, первый параметр функции в конструкторе Promise — функция resolve выполняется при успешном выполненим. В эту функцию обычно передается значение, которое представляет результат операции при успешном выполнении. Это значение может представлять любой объект. Например, передадим в эту функцию строку:
const myPromise = new Promise(function(resolve)< console.log("Выполнение асинхронной операции"); resolve("Привет мир!"); >);
Функция resolve() вызывается в конце выполняемой операции после всех действий. При вызове этой функции промис переходит в состояние fulfilled (успешно выполнено).
При этом стоит отметить, что теоретически мы можем возвратить из функции результат, но практического смысла в этом не будет:
const myPromise = new Promise(function(resolve, reject)< console.log("Выполнение асинхронной операции"); return "Привет мир!"; >);
Данное возвращаемое значение мы не сможем передать во вне. И если действительно надо возвратить какой-то результат, то он передается в функцию resolve() .
Передача информации об ошибке
Второй параметр функции в конструкторе Promise — функция reject вызывается при возникновении ошибки. В эту функцию обычно передается некоторая информация об ошибке, которое может представлять любой объект. Например:
const myPromise = new Promise(function(resolve, reject)< console.log("Выполнение асинхронной операции"); reject("Переданы некорректные данные"); >);
При вызове функции reject() промис переходит в состояние rejected (завершилось с ошибкой).
Объединение resolve и reject
Естественно мы можем определить логику, при которой в зависимости от условий будут выполняться обе функции:
const x = 4; const y = 0; const myPromise = new Promise(function(resolve, reject) < if(y === 0) < reject("Переданы некорректные данные"); >else < const z = x / y; resolve(z); >>);
В данном случае, если значени константы y равно 0, то сообщаем об ошибке, вызывая функцию reject() . Если не равно 0, то выполняем операцию деления и передаем результат в функцию resolve() .
Promise.all()
Метод Promise.all(iterable) возвращает промис, который выполнится тогда, когда будут выполнены все промисы, переданные в виде перечисляемого аргумента, или отклонено любое из переданных промисов.
Синтаксис
Promise.all(iterable);
Параметры
Перечисляемый объект, например, массив ( Array ). Смотрите iterable (en-US) .
Возвращаемое значение
Promise , который будет выполнен когда будут выполнены все промисы, переданные в виде перечисляемого аргумента, или отклонён, если будет отклонено хоть одно из переданных промисов.
Описание
Promise.all возвращает массив значений от всех промисов, которые были ему переданы. Возвращаемый массив значений сохраняет порядок оригинального перечисляемого объекта, но не порядок выполнения промисов. `Если какой-либо элемент перечисляемого объекта не является промисом, то он будет преобразован с помощью метода Promise.resolve .
Если одно из переданных промисов будет отклонено, Promise.all будет немедленно отклонён со значением отклонённого промиса, не учитывая другие промисы, независимо выполнены они или нет. Если в качестве аргумента будет передан пустой массив, то Promise.all будет выполнен немедленно.
Примеры
Использование Promise.all
Promise.all ждёт выполнения всех промисов (или первого метода reject() ).
var p1 = Promise.resolve(3); var p2 = 1337; var p3 = new Promise((resolve, reject) => setTimeout(resolve, 100, "foo"); >); Promise.all([p1, p2, p3]).then((values) => console.log(values); >); //Выведет: // [3, 1337, "foo"]
Promise.all поведение немедленного отклонения
Promise.all будет немедленно отклонён если один из переданных промисов будет отклонен: если у вас есть четыре промиса которые будут выполнены с задержкой и один, который будет отклонен немедленно — тогда Promise.all будет немедленно отклонён.
var p1 = new Promise((resolve, reject) => setTimeout(resolve, 1000, "one"); >); var p2 = new Promise((resolve, reject) => setTimeout(resolve, 2000, "two"); >); var p3 = new Promise((resolve, reject) => setTimeout(resolve, 3000, "three"); >); var p4 = new Promise((resolve, reject) => setTimeout(resolve, 4000, "four"); >); var p5 = new Promise((resolve, reject) => // Этот промис прервёт Promise.all reject("reject"); >); Promise.all([p1, p2, p3, p4, p5]).then( (value) => console.log(value); >, (reason) => console.log(reason); >, ); //Выведет: //"reject"
Спецификации
Specification |
---|
ECMAScript Language Specification # sec-promise.all |
Совместимость с браузерами
BCD tables only load in the browser
Promises — JS: Синхронная асинхронность
Промисы стали настоящим спасением человечества и среди прогрессивных разработчиков являются основным способом управления асинхронным кодом.
Полное описание всех возможностей и аспектов поведения промисов является объемной задачей, которая может запутать на первых порах, поэтому в этом уроке мы остановимся на ключевых особенностях поведения. Все остальное можно почерпнуть из стандарта и/или документации.
Знакомству с промисами способствует понимание темы «конечные автоматы».
Начнем по традиции с примера:
const file = '/tmp/hello1.txt'; import writeFile, readFile > from 'fs-promise'; writeFile(file, 'hello world') .then(() => readFile(file, 'utf8')) .then(contents => console.log(contents)) .catch(err => console.log(err)); // hello world
В этом примере происходит запись файла, затем чтение этого же файла, затем вывод содержимого этого файла в консоль, а в случае ошибки, возникшей на любом этапе, она была бы выведена на экран.
Абзац выше – это пример того, как выглядит типичная программа, построенная на промисах. Так что такое промис?
Объект, используемый для асинхронных операций. Промис содержит в себе результат выполнения и позволяет строить цепочки из вычислений, избегая проблемы callback hell
- Promise.prototype.then(onFulfilled, onRejected)
- Promise.prototype.catch(onRejected)
Отсутствие callback hell происходит благодаря тому, что мы всегда работаем на уровне последовательных вызовов then , а не уходим в глубину.
Разберем пример выше по косточкам. Первый вызов writeFile(file, ‘hello world’) возвращает тот самый промис, и пока не важно, как он строится внутри, сейчас мы пытаемся понять то, как с ним работать.
// Вызов ничем не отличается кроме того, что мы не передаем колбек writeFile(file, 'hello world')
После этого у нас есть два варианта:
- Мы вызываем then и передаем функцию onFulfilled , которая будет вызвана в случае успешного выполнения асинхронной операции
- Мы вызываем catch и передаем функцию onRejected , которая будет вызвана, в случае ошибок в результате выполнения асинхронной операции.
Функция onFulfilled принимает на вход данные, которые были получены в результате предыдущего выполнения. Таким образом идет передача данных по цепочке.
.then(() => readFile(file, 'utf8')) .then(contents => console.log(contents))
Данные, возвращаемые из функции onFulfilled , переходят по цепочке в функцию onFulfilled следующего then . Но если вернуть promise , то в следующем then окажутся данные, полученные в результате выполнения этого промиса, а не сам промис. Что и происходит в примере выше: мы возвращаем readFile() , а ниже получаем contents . То есть, промисы хорошо комбинируются друг с другом.
Конечный автомат
Теперь попробуем посмотреть внутрь промиса. С концептуальной точки зрения промис – это конечный автомат, у которого три состояния: pending , fulfilled , rejected .
Изначально он находится в состоянии pending , а дальше может перейти в одно из двух: либо выполнен ( fulfilled ), либо отклонен ( rejected ). И все, больше никакие переходы невозможны. Придя один раз в одно из терминальных (конечных) состояний, промис больше не подвержен изменениям, как бы мы не старались снаружи заставить его перейти в другое состояние.
Реализация
const promiseReadFile = filename => return new Promise((resolve, reject) => fs.readFile(filename, (err, data) => err ? reject(err) : resolve(data); >); >); >;
Любая функция возвращающая промис, внутри себя создает объект промиса привычным способом. Конструктор Promise принимает на вход функцию, внутри которой запускается выполнение асинхронной операции. Делается это, кстати, сразу, промисы не являются примером отложенного (lazy) выполнения кода. Но это еще не все. Промис требует от нас некоторых действий для своей работы. Во входную функцию передаются две другие: reject и resolve . reject должна быть вызвана в случае ошибки с передачей внутрь объекта error , а resolve — в случае успешного завершения асинхронной операции с передачей внутрь данных, если они есть.
Ошибки
Ошибка обрабатывается ближайшим обработчиком onRejected в цепочке вызовов. При этом существует два варианта определения обработчика. Первый — через catch , второй — с помощью передачи в then второго параметра. Это продемонстрировано в примере ниже:
promiseReadFile('file1') .then(data => promiseWriteFile('file2', data)) .then(() => promiseReadFile('file3')) .then(data => console.log(data)) .catch(err => console.log(err)); // .then(null, err => console.log(err));
Promise.all
Иногда возникает необходимость дождаться выполнения нескольких асинхронных операций. В этом случае можно воспользоваться идиомой Promise.all . Работает она очень просто: в эту функцию передается массив промисов, а дальше в then приходит массив с результатами выполнения.
const readJsonFiles = filenames => // N.B. passing readJSON as a function, // not calling it with `()` return Promise.all(filenames.map(readJSON)); > readJsonFiles(['a.json', 'b.json']) .then(results => // results is an array of the values
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
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);