ECMAScript 6 Promises

в 10:45, , рубрики: ECMAScript, ecmascript 6, javascript, promises, promises/a+, никто не читает теги

На Хабре уже встречались статьи о замечательной технологии Promises, которая в будущем станет частью стандарта ECMAScript 6, однако, в этих статьях не было подробного описания, почему же они так полезны и в чем таки их преимущества. Дабы заполнить этот пробел, я решил написать эту статью.

Внимание! Данная статья — не исчерпывающее руководство. Это скорее пиар хорошей и полезной технологии, замануха, показывающая позитивные стороны. Статья является компиляцией нескольких чужих статей, ссылки внизу.

Итак, что же такое Promise?

  • Promise это объект, используемый как заглушка для результата некоего отложенного (и возможно асинхронного) вычисления
  • Способ писать последовательно/параллельно выполняемый асинхронный код как синхронный
  • Часть стандарта ECMAScript 6

Важные основы

  • Конструктор Promise — это имплементация паттерна Revealing Constructor Pattern
  • «Статичные» методы
  • Promise.resolve(val) and Promise.reject(val)
  • Promise.all and Promise.race
  • Концепция “Thenable” object
  • В двух словах — это объект, обладающий методами .then() and .catch()

Состояния Promise

  1. Fulfilled — вычисление было успешным
  2. Rejected — ошибка при вычислении (любая)
  3. Pending — вычисление еще не завершилось (не fulfilled и не rejected)
  4. Settled — вычисление завершилось (не важно как)

Примеры

Итак, возьмем кусок синхронного кода

function foo() {
	var a = ‘a’;
	a = a + ‘b’;
	a = a + ‘c’;
	return a;
}

И сделаем так, чтобы снаружи он выглядел как Promise:

function foo() {
	var a = ‘a’;
	a = a + ‘b’;
	a = a + ‘c’;
	return Promise.resolve(a);
}

Следующий шаг — раздели каждый этап вычисления:

function foo() {
	return Promise.resolve(‘a’)
		.then(function(a){ return a + ‘b’; })
		.then(function(a){ return a + ‘c’; });
}

Теперь каждый шаг можно сделать асинхронным, причем все выполнение будет по-прежнему последовательным.
Пойдем дальше, заменим один из шагов на «как бы асинхронную функцию, возвращающую Promise»:

function getB(a){ return Promise.resolve(a + ‘b’); }

function foo() {
	return Promise.resolve(‘a’)
		.then(function(a){ return getB(a); })
		.then(function(a){ return a + ‘c’; });
}

Встроенная функциональность допускает возврат в then(cb()) либо ошибки (throw new Error()) либо значения (return a+'c';) либо следующего Promise.

Параллелизм

Представим, что сперва надо выполнить асинхронное действие 1, затем параллельно 2 и 3, и затем 4.

asyncAction1()
	.then(function(res1){
		return Promise.all([async2(res1), async3(res1)]);
	}).
	.then(function(arr){ // an array of values
		var res2 = arr[0], res3 = arr[1];
		return asyncAction4(res2, res3);
	}).
	then(…);

Обработка ошибок

Самая замечательная вещь в Promises — это обработка ошибок. Не важно, на каком этапе и в какой глубине вложенности произошла ошибка, будь то reject или просто брошенное исключение, все это можно поймать и обработать, либо же прокинуть дальше.

asyncAction()
	.catch(function(rejection){
		// пытаемся понять, можно ли как-то обработать ошибку 
		if (rejection.code == ‘foo’) return ‘foo’;
		// никак нельзя, прокидываем ошибку дальше
		throw rejection;
	})
	.then(…) 
	.then(…)
	.catch(…);

Здесь нужно сделать замечание, что если, положим

var p1 = new Promise(...),
    p2 = new Promise(...)
    p3 = Promise.all([p1, p2]);

p3.then(...).catch(...);

Catch будет ловить все, что пошло не так (причем не важно, что и как именно) в p1, p2, p3 и любых вложенных вызовах, что дико удобно. Обратная сторона — если catch() нет, то ошибка будет тихо проглочена. Однако библиотеки типа Q, как правило, имеют возможность задать обработчик непойманных ошибок, где их можно вывести в консоль или сделать что-то еще.

Слово об анти-паттернах

function anAsyncCall() {
	var promise = doSomethingAsync(); 	promise.then(function(){
		somethingComplicated();
	});
	return promise;
}

Иии легким движением руки мы потеряли второй Promise. Дело в том, что каждый вызов .then() или .catch() создает новый Promise, поэтому если создали новый, а вернули старый, то новый повиснет где-то в воздухе и никто не узнает, каков результат вычисления. Как бороться — просто вернуть новый Promise:

return promise.then(...);

Полезные ништяки

Задержка выполнения

function delay(ms){
	return new Promise(function(resolve){
		setTimeout(resolve, ms);
	}
};

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

delay(5000).then(…);

Простейший таймаут

Поскольку Promise может быть settled только один раз (остальное игнорируется), то можно написать что-то вроде

function timeout(promise, ms) {
	return new Promise(function (resolve, reject) {
		promise.then(resolve);
		setTimeout(function () {
			reject(new Error('Timeout’));
		}, ms);
	});
}

timeout(asyncAction(), 5000).then(…).catch(…);

Кто первый встал — того и тапки.

Немного улучшенный таймаут

Чуть более очевидный пример таймаута через «статичную» функцию Promise.race().

Promise.race([
	asynchronousAction(),
	delay(5000).then(function () {
		throw new Error('Timed out');
	})
])
.then(function (text) { ... })
.catch(function (reason) { ... });

Важное замечание об асинхронности

  1. Библиотека контролирует процесс выполнения, соответственно она заведует, как доставляется результат — синхронно или асинхронно
  2. В то же время, спецификация Promises/A+ требует, чтобы всегда использовался последний режим — асинхронный
  3. Таким образом, мы всегда можем полагаться на немедленное выполнение кода promise.then().catch() и т.д., и не заботиться, что какой-то их коллбеков съест все процессорное время (с оговорками, само собой)

Почему Promises лучше callbacks

  • Они часть стандарта — умные люди думали и разрабатывали всякое для нашего удобства
  • «Практически» не сказываются на производительности (http://thanpol.as/javascript/promises-a-performance-hits-you-should-be-aware-of)
  • Попробуйте разрулить нетривиальный поток выполнения на callbacks, желательно с обработкой ошибок, если вы не напишете свою имплементацию Promises, код, скорее всего, будет невозможно читать и понимать
  • Все Promises/A+ совместимые библиотеки могут принимать объекты друг друга (Angular прекрасно работает с объектами Q/Native Promise/RSVP и т.д.)
  • А еще Promise — лучше, чем Deferred, потому что последний — это две концепции в одной, что безусловно плохо, а кроме этого, паттерн Revealing Constructor почти что гарантирует, что Promise будел settled только в рамках этого конструктора (ну или вы сам себе злобный Буратино)

Недостатки

  1. Не подходит для повторяющихся событий (правда, Promises не для того и писались)
  2. Не подходит для streams (аналогично)
  3. Текущая реализация в браузерах не позволяет следит за progress (собираются тоже включить в стандарт)

Замечание о jQuery

Реализация «Thennable» в jQuery немного отличается от стандартной — в стандарте аргумент в callback ровно один, в jQuery кол-во аргументов больше, что не мешает использовать объект, полученный из jQuery в нативной реализации. Как правило, больше, чем первый аргумент, ничего от jQuery и не нужно. Плюс существует конструкция:

var jsPromise = Promise.resolve($.ajax('/whatever.json'));

Ссылки для самостоятельного чтения

Автор: dfuse

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js