НЕ МОЛЧИ!!!    Сделай что-нибудь,
чтобы остановить войну России в Украине.
...бойтесь людей равнодушных - именно с их молчаливого согласия происходят все самые ужасные преступления на свете.   ("Репортаж с петлёй на шее")

Promise

Описывается класс Promise, на котором построено современное API асинхронного взаимодействия, приведены примеры использования.

Содержание

  • Что такое промис?
  • Методы промиса
  • Преимущества промисов
  • Пример использования
  • Дополнительные возможности
  • Итоги

Что такое промис?

Promise — это специальный Javascript объект, введенный стандартом ECMAScript2015. С его помощью строится взаимодействие скриптов с отложенными данными, т.е. данными, которые не могут быть получены немедленно. Это может быть запрос по сети, чтение объемного файла или какие-либо трудоемкие длительные вычисления. При создании промиса вместо требуемых данных возвращается объект, который обещает их предоставить, как только они будут получены. Отсюда и название promise — обещание.

Поскольку это объект, то и создается он с помощью конструктора:

var promise = new Promise(function(resolve, reject) {...});

Параметр конструктора — это функция, которая осуществляет запрос данных. Её часто называют исполнитель(executor). Код этой функции предоставляет пользователь промиса. При создании промиса эта функция запускается на исполнение, т.е. производит запрос данных. Этому исполнителю в свою очередь передаются два параметра, resolve и reject, эти параметры тоже функции. Их назначение:
— resolve — запускается промисом при успешном завершении запроса.
— reject — запускается промисом при неудачном завершении запроса, т.е., когда запрошенные данные получить не удалось.
Как resolve, так и reject — это внутренние функции промиса, их писать не надо. Эти функции могут принять один входной параметр. Для resolve это, как правило, принятые промисом данные. Для reject, как правило, информация об ошибке. Но это не обязательное условие, передавать можно любой параметр.

Как работает промис

Если присмотреться, промис фактически  является оберткой над результатом запроса. Назначение промиса — произвести запрос, дождаться отклика и связать пришедшие данные с тем кодом, который их запросил. Важно следующее, все перечисленные операции производятся в другом потоке исполнения, поэтому основной код не зависает, ожидая данных. Под откликом здесь понимается любой ответ сервера, как успешный, так и неудачный.
Из приведенного описания можно догадаться, как же все работает. Есть код, которому нужны некие данные, для получения которых требуется достаточно длительное время. Этот код создает промис, передает ему функцию-исполнитель, которая производит запрос данных. При успешном завершении запроса промис запускает функцию resolve, в параметры которой будут переданы пришедшие данные. Если в результате запроса возвращается ошибка, то промис запускает функцию reject, которой обычно передается информация, поясняющая тип ошибки.
Другими словами, при создании промиса, мы указываем ему, как получить данные от источника(выполняется executor), и что сделать с откликом при любом результате — успешном или ошибочном. Из приведенного вытекает, что после создания промиса можно заниматься выполнением основном кода не ожидая результатов запроса, т.к. при получении отклика, промис самостоятельно распорядится им в соответствии со скриптом, прописанном в функциях resolve и reject. 

Схематичный пример промиса

 Вот пример отображающий принцип работы промиса.

const promise = new Promise((resolve, reject) => {
// 1. Запускаем асинхронную операцию
someAsyncRequest((error, response) => {

// 2. Всегда обрабатываем результат ВНУТРИ колбэка
if (error) {
reject(error);
return; // Выходим из колбэка, чтобы не выполнять код ниже
}

if (response.state === 'OK') {
resolve('Данные получены: ' + response.data);
} else {
reject(new Error('Ошибка при запросе данных'));
}
});
});

В процессе получения данных промис может находиться в трех состояниях, описываемых внутренним свойством state:
   ожидание(pending) — его начальное состояние, когда он ожидает завершения асинхронной операции;
   исполнено(fulfilled) — переходит в это состояние в результате успешного завершения асинхронной операции;
   отклонено(rejected) — переходит в это состояние, если получение данных завершилось с ошибкой.
Состояние промиса изменяется только один раз, а это значит, что из состояния ожидания он может перейти только в состояние успех или ошибка. Из этого следует, что при любом завершении асинхронного процесса, успешном или неудачном, будет вызвана только одна из функций переданных параметру executor при создании промиса.

Методы промиса

У промиса есть внутренние свойства state, result и error. Они недоступны пользователю напрямую, но доступ к ним возможен через методы промиса .then, .catch, и .finally. Разберемся с ними.

promise.then()

В параметры метода then передаются две функции, одна исполняется при удачном завершении запроса, другая — при неуданом. И одна из них обязательно будет исполнена, ведь промис может завершиться либо успешно, либо неудачно:

promise.then(
  	function(result) { 
	/* функция обработки успешного выполнения */ 
	},
  	function(error) { 
		  /* функция обработки ошибки */
	});

В параметры этих callback функций обычно передаются данные, полученные при завершении запроса, в первую — при успешном завершении, во вторую — при получении ошибки в отклике. Также отметим, что второй параметр метода не обязателен.

promise.catch()

Если потребителя запроса интересует только ошибки взаимодействия, то в предыдущем методе можно первым аргументом указать null и передать функцию только во второй параметр: promise.then(null, f) или использовать другой, эквивалентный этому случаю, метод:

	promise.catch(error);

где error — это данные, переданные промисом в функцию reject при неудачном завершении запроса. В качестве данных можно передавать любой тип,  но настоятельно рекомендуется передавать объект ошибки (new Error()), что облегчает отладку кода.

promise.finally()

По аналогии со стандартным блоком обработки ошибок javascript промис имеет метод finally, который гарантированно вызывается при любом завершении взаимодействия:

	promise.finally();

Этот метод предназначен для того, чтобы очистить память от более не нужных ресурсов и корректно завершить запрос при любом его результате, поэтому ему не передаются никакие параметры. Более того, метод не знает результат запроса. Его даже можно запускать первым при любом финале, т.к. он не блокирует передачу результата или ошибки другим методам, например, .then.

Это не полный перечень методов промиса, здесь приведены лишь наиболее востребованные и необходимые.

Преимущества промисов

Может возникнуть справедливый вопрос, а зачем это надо, например, XMLHttpRequest прекрасно обеспечивает AJAX взаимодействие?

Использование промисов, облегчает труд программиста, упрощая код и делая его более наглядным. Но, на мой взгляд, наибольшая польза промисов в том, что с их помощью проще производить цепочку асинхронных операций одна за другой, где каждая следующая может использовать результат предыдущей(promise chain). В отличие от AJAX с его callback функциями, здесь все сконцентрировано в одном объекте, а не размазано по листингу во вложенных блоках, т.е., позволяет уйти от так называемого «callback hell», когда при необходимости получения последовательности запросов усложняется программный код из-за большой вложенности callback функций.
Однако пора переходить к практике.

Пример использования Promise

В примере ниже мы с помощью промиса запросим с сервера текстовое сообщение, для наглядности определим задержку, с которой нам должен приходить отклик.

Что в примере?
Имеем кнопку «Получить СМС» с id = «sms_request»,  контейнер для отображения сообщений с id = «txt_container», ввод величины задержки с id = «delay» и нижеприведенные скрипты.

cons promise;

// обработчик кнопки "Получить СМС", запрашивает сообщение с сервера
function getMsg()
{
document.getElementById('txt_container').innerHTML = 'wait ...';
var url = baseUrl + "/ваш_путь_к_обработчику_запроса/getSms.php"
+ '/?delay=' + getDelay();
promise = getPromise(url);
promise.then(function (data) {
document.getElementById('txt_container').innerHTML = data;
});
promise.catch(function (err) {
document.getElementById('txt_container').innerHTML =
'Ошибка ' + err.status + ': ' + err.statusText;
});
};

// возвращает экземпляр promise
function getPromise(url)
{
return new Promise((resolve, reject) => {
var request = new XMLHttpRequest();
request.open('GET', url);
request.onload = function() {
if (request.readyState === 4 && request.status === 200) {
resolve(request.response);
} else{
reject({
status: this.status,
statusText: request.statusText
});
}
}
// передает в промис сообщение, когда запрос не был выполнен
request.onerror = function() {
var err = new Error();
err.status = '-1';
err.statusText = "Запрос не выполнен";
reject(err);
};
request.send();
});
};

// возвращает величину задержки ответа сервера
function getDelay()
{
var del = document.getElementById('delay');
return document.getElementById('delay').value;
}

Комментарий к примеру.
На мой взгляд XMLHttpRequest запрос прекрасно справляется с получением отклика, а наш промис выглядит ненужной надстройкой.  Это как раз и подтверждает ранее высказанное утверждение, что промис удобно использовать при сложном многоступенчатом асинхронном взаимодействии, когда требуется цепочка длительных, зависящих один от другого, запросов.

Дополнительные возможности

Были рассмотрены наиболее востребованные возможности промиса, но это далеко не все. Стоит упомянуть статические методы промиса, которые могут быть весьма полезны:
  — Promise.all(arr) —
на входе получает массив промисов, возвращает новый промис. Этот новый промисс завершится, после завершения последнего промиса в массиве, его результатом будет массив результатов входных промисов. Удобно, не правда ли? В выходном массиве отклики находятся в том же порядке, что и исходные промисы.
— Promise.race(arr) — на входе также получает массив промисов, но завершается после завершения самого «быстрого» промиса с любым результатом, успешным или с ошибкой.
   — Promise.any(arr) — возвращает первый успешно выполненный промис. Если ни один из промисов не завершился успешно, то возвращается промисс с объектом ошибки AggregateError
   — Promise.allSettled — возвращает массив объектов, где объекты это все промисы, завершившиеся как успешно, так и неудачно.
   — Promise.resolve(value) — возвращает успешно выполненный промис, т.е. результат тот же, что и у кода

let promise <code class="yoast-text-mark">ass=">=&lt;/code> <code class="> =></code> <code>ass="token function">resolve(</code>values="token punctuation">));</code>&lt;/pre>

— Promise.resolve(value) — возвращает промис с ошибкой error

let promise = new Promise((resolve, reject) => reject(error));

Итоги

Итак, мы рассмотрели объект promise, который лежит в основе асинхронного взаимодействия приложений на современном этапе. За счет инкапсуляции в себе запрашивающего и потребляющего программных кодов, он обеспечивает их взаимодействие и упрощает жизнь программиста.