Promise

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

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

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

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

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

где

параметр конструктора — это функция, в которой представлен код так называемой функции-исполнителя(executor), этот код обеспечивает асинхронный запрос данных. При создании промиса функция автоматически запускается на исполнение;
resolve — параметр исполнителя, callback функция исполнителя, запускаемая промисом при успешном завершении запроса;
reject — callback функция исполнителя, исполняемая промисом при неудачном завершении функции-исполнителя, т.е., когда запрошенные данные получить не удалось.

Важное примечание. В эти callback функции можно передавать любые параметры.

Как работает промис можно догадаться из описания параметров. При его создании запускается функция-исполнитель, которая реализует асинхронный процесс получения данных, при успешном их получении запускается функция resolve, в параметры которой передаются нужные данные, если вместо запрошенного ресурса возвращается ошибка, то запускается функция reject, которой, как правило, передается информация, поясняющая тип ошибки.

Вот чуть более детальный пример создания промиса.

	var promise = new Promise( (resolve, reject) => {
   	//здесь код асинхронной операции ...
 	//допустим, результат исхода операции сохраняется в переменной status
		if (status === 'OK') {
		    resolve('Данные получены');
		} else {
		    reject('Ошибка 404');
		}   
	});

Промис может находиться в трех состояниях, описываемых внутренним свойством 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 при неудачном завершении запроса, как правило это информация об ошибке.

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» и нижеприведенные скрипты.

var 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;
          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, который лежит в основе асинхронного взаимодействие приложений на современном этапе. За счет инкапсуляции в себе запрашивающего и потребляющего программных кодов, он упрощает процесс такого взаимодействия.