image

Этот материал мы подготовили для JavaScript-программистов, которые только начинают разбираться с «Promise». Обещания (promises) в JavaScript – это новый инструмент для работы с отложенными или асинхронными вычислениями, добавленный в ECMAScript 2015 (6-я версия ECMA-262).

До появления «обещаний» асинхронные задачи можно было решать с помощью функций обратного вызова или с помощью обработки событий. Универсальный подход к решению асинхронных задач – обработка событий. Менее удобный, но также имеющий право на существование, способ использовать функции обратного вызова. Конечно, выбор решения зависит от стоящей перед вами задачи. Вариант решения задач с помощью «обещаний», скорее, призван заменить подход к функциями обратного вызова.

В использовании функций обратного вызова есть существенный недостаток с точки зрения организации кода: "callback hell". Этот недостаток заключается в том, что в функции обратного вызова есть параметр, который, в свою очередь, также является функцией обратного вызова – и так может продолжаться до бесконечности.



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

Давайте рассмотрим работу «обещаний» на примере конкретной задачи:
После загрузки страницы браузера необходимо показать изображения из указанного списка.

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

/**
 * список изображений
 * (предположим, что изображения 1.jpg, 2.jpg, 3.jpg, 4.jpg существуют, а
 * fake.jpg - нет)
 * 
 * @type {string[]}
 */
var imgList = ["img/1.jpg", "img/2.jpg", "img/fake.jpg", "img/3.jpg", "img/4.jpg"];

Сначала напишем функцию, которая подгружает одно изображение по указанному url.

function loadImage(url)
/**
 *
 * подгружаем изображение по указанному url
 *
 * @param url
 * @returns {Promise}
 */
function loadImage(url)
{
	//объект "обещание"
	return new Promise(function(resolve, reject)
	{
		var img = new Image();
		img.onload = function()
		{
			//в случае успешной загрузки изображения, результат "обещания" будет url этого изображения
			return resolve(url);
		}
		img.onerror = function()
		{
			//в случае не успешной загрузки изображения, результат "обещания" будет url этого изображения
			return reject(url);
		}
		img.src = url;
	});
}


Объект «обещание» создается с помощью конструктора new Promise(...), которому в качестве аргумента передается анонимная функция с двумя параметрами: resolve, reject. Они, в свою очередь, так же являются функциями. Resolve() — сообщает о том, что код выполнен «успешно», reject() – код выполнен с «ошибкой» (что считать «ошибкой» при выполнении вашего кода, решать вам. Это что-то вроде if(true){...} else {...}).

Интерфейс Promise (обещание) представляет собой обертку для значения, неизвестного на момент создания обещания. Он позволяет обрабатывать результаты асинхронных операций так, как если бы они были синхронными: вместо конечного результата асинхронного метода возвращается обещание, результат которого можно получить в некоторый момент в будущем.

При создании обещание находится в ожидании (состояние pending), а затем может стать выполнено (fulfilled), вернув полученный результат (значение), или отклонено (rejected), вернув причину отказа.

В методы resolve() и reject() можно передавать любые объекты. В метод reject(), как правило, передают объект типа Error с указанием причины ошибки («отклоненного» состояния «обещания»). В любом случае, это не обязательно. Решение, как дальше вы будете обрабатывать такие ситуации – за вами.

На данный момент может показаться, что «обещание» совершенно не нужно использовать в этой ситуации. Пока мы лишь устанавливаем некий индикатор того, было ли подгружено изображение. Однако вскоре вы увидите, что этот механизм может легко, интуитивно понятно определять, что произойдет после того, как задача будет выполнена (изображение подгружено или нет).

Методы then() и catch()


Всякий раз, когда вы создаете объект «обещание», становятся доступны два метода: then() и catch(). Используя их, вы можете выполнить нужный код при успешном разрешении «обещания» (resolve(...)) или же код, обрабатывающий ситуацию с «ошибкой» (reject(...)).

then() и catch()
function myPromise()
{
	return new Promise(function(resolve, reject)
	{
		//псевдо асинхронный код
		var ascync = true; //или  false
		if (!ascync)
		return reject(new Error("не удалось выполнить..."));

		return resolve(1);
	});
}

myPromise()
.then(function(res)
{
	console.log(res); //выведет 1
})
.catch(function(err){
	console.log(err.message); //выведет сообщение "не удалось выполнить..."
});

Примечание: не обязательно возвращать (return) resolve(...) или reject(...):. В примере выше можно было бы написать так:

//псевдо асинхронный код
var ascync = true; //или  false
if (!ascync)
{
	reject(new Error("не удалось выполнить..."));
}
else
{
	resolve(1);
}


В результате вызова myPromise() все равно сработал бы метод then() или catch(). Лучше всего завести сразу привычку — всегда возвращать resolve(...) или reject(...). В будущем это поможет избежать ситуации, когда код будет работать не так, как ожидается.

В методы then() и catch() передают две анонимные функции. Синтаксис метода then() в общем случае такой:

then(function onSuccess(){}, function onFail(){});

Параметр function onSuccess(){} будет вызван в случае успешного выполнения «обещания», function onFail(){} – в случае ошибки. По этой причине следующий код будет работать одинаково:

Примеры метода then()
myPromise()
.then(function(res)
{
	console.log(res); //выведет 1
})
.catch(function(err){
	console.log(err.message); //выведет сообщение "не удалось выполнить..."
});

myPromise()
.then(function(res)
{
	console.log(res); //выведет 1
},
function(error)
{
	console.log(err.message); //выведет сообщение "не удалось выполнить..."
});

myPromise()
.then(function(res)
{
	console.log(res); //выведет 1
})
.then(undefined, function(error)
{
	console.log(err.message); //выведет сообщение "не удалось выполнить..."
});


Гораздо привычнее и понятнее использовать catch(...). Также метод catch() можно вызывать «посередине» цепочки вызовов then(), если логика вашего кода того требует: then().catch().then().Не забывайте вызывать catch() последним в цепочке: это позволит вам всегда отлавливать «ошибочные» ситуации.

Вызовем наш метод loadImage(url) и для примера добавим одну картинку на страницу:

//считаем, что на странице есть элемент с id="images", например, div
loadImage(imgList[0])
.then(function(url)
{
	$('#images').append('<img src="'+url+'" style="width: 200px;" />');
})
.catch(function(url)
{
	//как и сообщалось выше, не обязательно, чтобы сюда передавался объект типа Error
	//например, вы захотите сохранить в отдельный массив пути к картинкам , которые не подгрузились, и потом что-нибудь с ним сделать...
	console.log("не удалось загрузить изображение по указанному пути: ", url);
});

Последовательная рекурсивная подгрузка и отображение изображений


Напишем функцию для последовательного отображения изображений:

function displayImages(images)
/**
 * последовательная рекурсивная подгрузка и показ изображений
 * 
 * @param images - массив с url
 */
function displayImages(images)
{
	var imgSrc = images.shift(); // проходим по массиву с изображениями
	if (!imgSrc) return; //если в результате рекурсии прошлись по всему массиву

	//если в массиве еще есть изображение, загружаем его
	return loadImage(imgSrc)
	.then(function(url)
	{
		$('#images').append('<img src="'+url+'" style="width: 200px;"/>');
		return displayImages(images); //рекурсия
	})
	.catch(function(url)
	{
		//если какое-то из изображений не загрузилось, переходим к следующему изображению
		console.log('не удалось загрузить изображение по указанному пути: ', url);
		return displayImages(images); //рекурсия
	});
}


Функция displayImages(images) последовательно проходит по массиву с url изображений. В случае успешной подгрузки мы добавляем изображение на страницу и переходим к следующему url в списке. В противоположном случае – просто переходим к следующему url в списке.

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

var promiseImgs = [];
promiseImgs = imgList.map(loadImage);

//для наглядности
promiseImgs = imgList.map(function(url){
	return loadImage(url);
});

В массиве promiseImgs теперь находятся «обещания», у которых состояние может быть как «разрешено» так и «отклонено», так как изображения fake.jpg физически не существует.

Для завершения задачи можно было бы воспользоваться методом Promise.all(...).
Promise.all(iterable) возвращает обещание, которое выполнится после выполнения всех обещаний в передаваемом итерируемом аргументе.

Однако у нас в списке есть изображение, которого физически не существует. Поэтому методом Promise.all воспользоваться нельзя: нам необходимо проверять состояние объекта «обещание» (resolved | rejected).

Если в массиве «обещаний» есть хотя бы одно, которое «отклонено» (rejected), то метод Promise.all так же вернет «обещание» с таким состоянием, не дожидаясь прохождения по всему массиву.

Поэтому напишем функцию loadAndDisplayImages.

Подгружаем изображения, и показываем их на странице все сразу


function loadAndDisplayImages
/**
 *
 * @param imgList - массив url
 * @returns {Promise}
 */
function loadAndDisplayImages(imgList)
{
	var notLoaded = [];//сохраним url, какие не были загружены
	var loaded = [];//сохраним url, какие были загружены
	var promiseImgs = imgList.map(loadImage);

	//вернем результат работы вызова reduce(...) - объект Promise, чтобы можно было потом  при необходимости продолжить цепочку вызовов:
	//loadAndDisplayImages(...).then(...).catch(...);
	return promiseImgs.reduce(function (previousPromise, currentPromise)
	{
		return previousPromise
			.then(function()
			{
				//выполняется этот участок кода, так как previousPromise - в состоянии resolved (= Promise.resolve())
				return currentPromise;
			})
			.then(function(url) //для "обещаний" в состоянии resolved
			{
				$('#images').append('<img src="'+url+'" style="width: 200px;"/>');
				loaded.push(url);
				return Promise.resolve(url);
			})
			.catch(function(url)//для "обещаний" в состоянии rejected
			{
				console.log('не удалось загрузить изображение по указанному пути: ', url);
				notLoaded.push(url);
				return Promise.resolve(url);
			});

	}, Promise.resolve())
		.then(function (lastUrl)
		{
			console.log('lastUrl:', lastUrl);

			let res = {loaded: loaded, notLoaded: notLoaded};

			//но мы вернем Promise, значение которого будет объект
			return Promise.resolve(res);
		});

}

loadAndDisplayImages(imgList)
.then(function(loadRes)
{
	console.log(loadRes);
})
.catch(function(err)
{
	console.log(err);
});


Можно посмотреть сетевую активность в браузере и убедиться в параллельной работе (для наглядности в Chrome была включена эмуляция подключения по Wi-Fi (2ms, 30Mb/s, 15M/s):



Разобравшись, как работать с Promise, вам будет проще понять принципы работы, например, с API Яндекс.Карт, или Service Worker – именно там они используются.

UPD: В статье не озвучил один важный момент, с которым, отчасти, был связан совет писать return resolve() или return reject().

Когда вызываются данные методы, «обещание» устанавливается в свое конечное состояние «выполнено» или «отклонено», соответственно. После этого состояние изменить нельзя. Примеры можно посмотреть в комментарии.
Поделиться с друзьями
-->

Комментарии (59)


  1. svboobnov
    09.12.2016 20:23

    Спасибо за краткое, но довольно ёмкое введение.
    Я с JavaScript сталкивался в 2004 году, потому мне полезно =)


  1. Fen1kz
    09.12.2016 21:30
    +1

    Интересно, а рассказы про промисы ещё актуальны? (Не сарказм, просто иногда подумываю, а не написать ли и мне статью, где я всё всем разжую про них. Однако боюсь что все уже знают и закидают шапками типа "добро пожаловать в 2012")


    1. RA_ZeroTech
      09.12.2016 21:44
      +1

      А почему им не быть актуальными? ) async await не так давно уж и появились, если на это намек в вопросе :) да и статья не об эволюции асинхронного программирования в целом, а только о ее части :)


      1. akzhan
        10.12.2016 03:48
        +1

        async/await при том не отменяют Promise, а скорее дополняют оные.


      1. Kain_Haart
        10.12.2016 06:14
        +1

        Промисы-то актуальны, вопрос в том, актуальны ли новые рассказы про промисы для новичков, и уж крайне спорно — называть промисы «новый инструмент» в декабре 2016


      1. Keyten
        10.12.2016 16:49

        Если честно, про промисы можно вообще всё рассказать буквально за минуту (или за абзац текста и кода), т.к. это просто-напросто иной синтаксис записи обработки событий. Так что меня удивляют большие статьи об этом. Сам научился работать с промисами, тупо проглядев кусок кода, в котором они использовались. Всё понятно и очевидно.
        Имхо, конечно.


        1. RA_ZeroTech
          11.12.2016 02:51

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


          1. Singapura
            12.12.2016 12:33

            Вот хоть бы один из «учителей», Рго-вёл-бы на примере «Hello» через все главы учебника JS меняя в динамике «цвета»,«буквы»,«шрифты»,«циклы»,«размеры»,… и под конец «выжал-бы» этот «Привет» через какой-нибудь порт по-битово… куда-нибудь…


            1. svboobnov
              12.12.2016 23:03

              Интересная мысль, надо попробовать. Как в том анекдоте: «Вы знаете, коллега, я третий год объясняю сопромат студентам, и даже сам всё полностью понял!» =).
              Singapura, у Вас странное написание слов: принято писать не «по-битово» а «побитово»,
              не «выжал-бы» а «выжал бы».
              Если же Вы пытались поддеть RA_ZeroTech, то зря: фразы «в чём-то» «кому-то» и «как-то так» «кем-то» пишутся именно так, с дефисом.


      1. MaxKorz
        12.12.2016 12:33

        Только хабре статьей, подобной вашей (о Promises для новичков и/или профессионалов), уже около десятка. Очередная статья конечно будет актуальна, только вот зачем в очередной раз всё разжовывать?


  1. RidgeA
    09.12.2016 21:38

    В результате вызова myPromise() все равно сработал бы метод then() или catch(). Лучше всего завести сразу привычку — всегда возвращать resolve(...) или reject(...). В будущем это поможет избежать ситуации, когда код будет работать не так, как ожидается.


    Можете пример привести?


    1. Apathetic
      09.12.2016 21:58

      Присоединяюсь к вопросу. Никакой необходимости возвращать resolve или reject нет. Напротив, их весьма удобно использовать в качестве коллбеков при «промисификации» всяких коллбековых апи, и никаких return там, разумеется, и в помине нет.


    1. RA_ZeroTech
      09.12.2016 22:11

      Начав писать ответ на вопрос, понял, что не акцентировал внимание на том, что после вызовов resolve() или reject() состояние «промиса» уже нельзя изменить. то есть, вот этот код в обоих случаях выведет фразу вида «Promise rejected», несмотря на то, что в первом варианте нет «return»

      resolve() или reject()
      function myPromiseRejected()
      {
      	return new Promise(function (resolve, reject)
      	{
      		var err = 'error';
      		if (err)
      			reject('Promise rejected');
      
      		resolve('Promise resolved');
      	});
      }
      
      function myPromiseRejected2()
      {
      	return new Promise(function (resolve, reject)
      	{
      		var err = 'error';
      		if (err)
      			return reject('Promise rejected2');
      
      		return resolve('Promise resolved2');
      	});
      }
      
      
      myPromiseRejected()
      	.catch(function(err){
      		console.log(err);
      	});
      myPromiseRejected2()
      	.catch(function(err){
      		console.log(err);
      	});
      


    1. RA_ZeroTech
      09.12.2016 22:27
      +1

      раз вызвали resolve или reject(), и «состояние промиса установлено», можно ошибочно предположить, что код, который следует после них, не будет выполнен. Но это не так. Пример:

      пример с return resolve или reject().
      function myPromise1()
      {
      	return new Promise(function (resolve, reject)
      	{
      		var err = true;
      		if (err)
      			reject('Promise rejected');
      
      		console.log('у нас же "ошибка", почему оказались здесь?');
      
      		resolve('Promise resolved');
      
      	});
      }
      
      myPromise1()
      	.then(function (res)
      	{
      		console.log(res);
      	})
      	.catch(function(err){
      		console.log(err);
      	});
      


      1. RidgeA
        10.12.2016 00:24

        console.log('у нас же «ошибка», почему оказались здесь?');

        потому что промис — это конечный автомат, вызов reject/resolve меняет состояние, но не завершает выполнение текущей функции.

        вот попробуйте:
        new Promise((resolve, reject) => {
        	setTimeout(() => {
        		console.log('111111');
        		resolve();
        		setTimeout(() => {
        			console.log('222222');			
        		});
        	});
        }).then(console.log.bind(null, '333333'));
        


        Вот еще можно глянуть: https://www.promisejs.org/implementing/


        1. RidgeA
          10.12.2016 00:35

          http://caniuse.com/#search=Promise


        1. RA_ZeroTech
          10.12.2016 00:48

          потому что промис — это конечный автомат, вызов reject/resolve меняет состояние, но не завершает выполнение текущей функции.

          совершенно верно :) именно это я и показал в примере


  1. magicstream
    10.12.2016 00:27

    а как дела обстоят с нативной поддержкой браузерами? уже все подтянулись?


    1. novrm
      10.12.2016 01:09

      При желании можете использовать, например bluebird.


      1. Apathetic
        12.12.2016 22:36

        Скорее даже не при желании, а при возможности — если позволяет «бюджет» (читай — запас по размеру кода), лучше использовать Bluebird, так как он намного быстрее нативных промисов.


    1. RA_ZeroTech
      10.12.2016 01:25

      Chrome с версии 32, Firefox с 29-ой, Opera с 19-ой, Safari с 7.1, IE который Edge.

      Поэтому стоит использовать полифилы…

      novrm рекомендует bluebird. Я, в свою очередь, тоже ей пользуюсь, в том числе и при написании серверного JS (NodeJS). Достаточно много полезных методов реализовано.


      1. jMas
        10.12.2016 11:53

        А не лучше ли использовать какой ни будь полифил, который добавляем только объект window.Promise, и только те методы которые гарантированно присутствуют в браузерах. Ибо если вы завязываетесь на bluebird и на методы которых нет в реализациях браузеров — вы завязываетесь на кастомную реализацию промисов, а не полифилите реализацию браузера.


        1. novrm
          10.12.2016 21:10

          Promise, если не ошибаюсь — это технология…
          Как она будет реализована — другое дело…
          Но главное — должны быть полностью соблюдены рекомендации, что присутствует в bluebird, но отсутствует в том же jQuery…


          1. jMas
            10.12.2016 21:50

            Bluebird Promise да, поддерживает стандарт, но и выходят за его рамки. Если тебе нужен полифил, то тебе нужен полифил со стандартным набором методов. Потому как полифил нужен ровно до того момента, когда он становится не нужным. Тогда вы выкидываете полифил в пользу браузерной реализации. Если вы взяли в качестве "полифила" Bluebird и начали использовать методы не входящие в стандарт — выкинуть такой "полифил" не получится, и в этом случае полифил перестает быть полифилом.


            1. novrm
              11.12.2016 00:42

              Извините, но можно «локально» использовать Promise посредством bluebird…
              Имею ввиду — использовать «стандартный» набор методов.

              Если хотите, вот пример возможной реализации (es6).

              /**
               * Import bluebird plugin.
               *
               * @link https://github.com/petkaantonov/bluebird/
               * @link http://bluebirdjs.com/docs/api-reference.html
               */
              import BowerAssetBluebirdPlugin from 'asset/bluebird/js/browser/bluebird.min';
              
              'use strict';
              
              /**
               * Promise class wrapper.
               */
              classLoader.autoload['BundleFramework/Promise/Promise'] = (function () {
                  /**
                   * Set private properties.
                   */
                  let _plugin = BowerAssetBluebirdPlugin;
              
                  /**
                   * Promise class.
                   */
                  return class {
              
                      /**
                       * Constructor. Create an instance.
                       *
                       * @param object config
                       */
                      constructor(config = Object(config = {})) {
                          Object.freeze(this);
                      };
              
                      /**
                       * Ajax action.
                       *
                       * @link   http://bluebirdjs.com/docs/coming-from-other-libraries.html#coming-from-jquery-deferreds
                       * @param  object options
                       * @return object new _plugin
                       */
                      ajax(options = Object(options = {})) {
                          let plugin = this.getPlugin();
              
                          return new plugin(function(resolve, reject) {
                              $.ajax(options).done(resolve).fail(reject);
                          });
                      };
              
                      getPlugin() {
                          return _plugin;
                      };
              
                  };
              })();
              export {classLoader};
              


              1. jMas
                11.12.2016 02:01

                По-моему, достаточно подключить глобально файл-полифил и использовать Promise как часть глобального окружения (global, window). По-моему, это как раз и идея полифилов "доукомплектовать окружение отсутствующими в данной версии браузера компонентами".
                В вашем же случаи, насколько я смог понять, используется какой то кастомный прелоадер компонентов, через который вы и получаете доступ к Promise. В общем то, реализацию можно будет подменить и через него, но используя Bluebird вы (или ваши коллеги) рискуете завязаться на нестандартные методы.
                И еще кейс, если вы будете исользовать стороннюю библиотеку, которая не знает о вашем прелоадере компонентов — а в браузере внезапно в глобальном скопе нет Promise — библиотека не заработает.


                1. novrm
                  11.12.2016 14:49

                  Не совсем понял ваши мысли об нестандартных методах?
                  Разработчики Bluebird как раз следуют стандарту…
                  Ну а «сахар» плагина — можете не использовать. Или явно его «выключить» — создав обертку над Bluebird (пример реализации которого я привел выше).

                  Кроме того — что такое стандарт? Это просто рекомендации…
                  Возможно «сахар» Bluebird завтра станет этим самым стандартом.


                  1. jMas
                    11.12.2016 15:43

                    Или явно его «выключить» — создав обертку над Bluebird (пример реализации которого я привел выше).

                    Обертка плоха, потому что использует систему резолвинга зависимостей, наподобие requirejs. Подключаешь стороннюю библиотеку, которая ожидает window.Promise и она не работает, например, в относительно стареньких браузерах. Поэтому нужен полифил который как раз создает недостающий объект окружения window.Promise.


                    Возможно «сахар» Bluebird завтра станет этим самым стандартом.

                    Стандарт принимает рабочая группа. И если там нет в планах добавлять методы из Bluebird, значит их не добавят.


                    Возможно «сахар» Bluebird завтра станет этим самым стандартом.

                    Например через пару лет мы захотим выкинуть поддержку браузеров где нет промисов, проводим рефакторинг и принимаем решение выкинуть полифил Bluebird Promise. Смотрим в код, и видим, что пару программистов заюзали по всему проекту его "сахар". Получится ли в этом случае выкинуть такой "полифил"?


                    1. novrm
                      11.12.2016 16:04
                      -1

                      Знаете, вы так мыслите, как будто ваш код будет работать без изменений 100 лет.
                      Через несколько лет относительно стареньких браузеров никто поддерживать не станет.

                      Более того, возможно через пару лет и самих промисов не станет…
                      Их заменят чем то еще более универсальным… Например — генераторами.

                      Именно потому (ИМХО) резонно использовать «обертки» над технологиями…
                      Дабы отделить их использование от реализации.

                      А если вы желаете использовать некую стороннюю библиотеку, которая в свою очередь ожидает window.Promise — это проблема это библиотеки, что она внутри не реализует полифил…
                      Всегда можно найти библиотеку, которая реализует полифил или самому ее допилить…


                      1. jMas
                        11.12.2016 17:11

                        Именно потому (ИМХО) резонно использовать «обертки» над технологиями…

                        А если вы желаете использовать некую стороннюю библиотеку, которая в свою очередь ожидает window.Promise — это проблема это библиотеки, что она внутри не реализует полифил…

                        Она не обязана, если библиотека ожидает современное окружение. Ваша цель — предоставить соответствующее окружение, или сэмулировать его. В идеальном мире все окружение должно быть описано в зависимостях, но исторически такого механизма у нас нет. То есть, мы привыкли, что у нас есть window, localStorage, location. Конечно в идеале необходимо сделать декроторы для каждого, но часто это избыточно.


                        Знаете, вы так мыслите, как будто ваш код будет работать без изменений 100 лет.

                        Ну 100 лет преувеличено, но десятки лет — это почти реальность.


                        1. novrm
                          12.12.2016 12:59

                          Для Proxy и прочих новых технологий тоже полифил найдете?
                          А если не найдете?
                          Сознаетесь, что полифил не универсальный механизм внедрения новых технологий? Наверно нет.


                          1. jMas
                            12.12.2016 16:12

                            Сознаетесь, что полифил не универсальный механизм внедрения новых технологий? Наверно нет.

                            Тут соглашусь.


              1. alQlagin
                11.12.2016 09:24

                На самом деле пока даже рекомендуется использовать Bluebird вместо нативных промисов. Если верить бенчмаркам, он в разы быстрее


                1. jMas
                  11.12.2016 10:33
                  -1

                  Не спорю, но если производительность Promise не сильно важна, я предпочитаю обстрагироваться и использовать нативные Promise или легкие полифилы. Потому как производительность нативной реализации могут существенно улучшить.
                  Просто в Bluebird, как я и писал, не нравится что API выходит за пределы стандарта, с одной стороны это хорошо, но как по мне, выглядит странно.


  1. w4r_dr1v3r
    10.12.2016 01:09

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


  1. slavugan
    10.12.2016 01:09

    А как вы думаете, господа, не станет ли реактивный подход (RxJS) заменой промисов в будущем?


  1. Rulexec
    10.12.2016 01:47

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

    Всё это дело понеслось, а тут пользователь нажимает на ссылку, старый контент уже никому не нужен, нужно загружать новый. Окей, создаём новый промис, повторяем накидывание обработчиков. Постойте-ка, у нас в процессе выполнения старый. И тут оказывается, что у Promises/A+ нет никаких вариантов обработки отмены промиса.

    Нам остаётся три пути:

    • В каждом обработчике проверять, а не устарел ли запрос на действие, может результат уже никому не нужен.
    • Сделать так, чтобы результат выполнения ВСЕГО промиса был проигнорирован (хотя и выполнен).
    • Использовать 3rd party промисы или пилить свои.


    Пока не особо думал об этом, на данный момент отошёл от промисов, давно не использовал, но пока выглядит так, что отмена должна найти стык, где промис ещё в процессе резолвинга, подменить ему then'ы на пустышки, и послать внутрь самого промиса сигнал отмены, который он может обработать, чтобы прекратить своё выполнение, если может (к примеру, если там промис, который просто спит сколько-то времени, то в обработчике отмены он может просто сделать clearTimeout).

    Это всё ещё можно снабдить специальным сахаром для обработки отмен, чтобы можно было обрамлять некоторые куски цепочки операций, чтобы обрабатывать некоторые специфические случаи.

    К примеру, есть метод, удаляющий некий item, возвращающий промис о завершении. Внутри он отправляет запрос на сервер, плюс делает какие-то действия с моделью. Пользователь жмёт «удалить», метод вызывается, промис начинает работу. Тут пользователь снова жмёт «удалить», очень резко, настолько резко, что запрос даже не успел уйти на сервер (окей, может, у нас есть какая-нибудь логика группировки запросов, или окна общения с сервером, поэтому он не уходит сразу). Тогда мы должны отменить промис удаления. Но произвёв отмену, нам бы хорошо знать, ушёл запрос на сервер или нет. Поэтому мы можем вставить в место, где совершается непосредственно запрос, специальный обработчик, например, .onCancel, который вызовется только когда чейн промисов в состоянии отмены и в него передастся, какое состояние у промиса, на который он непосредственно был накинут (т.е. начал он резолвится или ещё нет). Где каким-то образом куда-нибудь сообщим, что да как. И, например, если запрос уже ушёл, то нам нужно посылать второй в догонку, мол, «сервер, пользователь передумал это удалять, верни как было, пожалуйста».

    Я пока не могу придумать, как можно типизированно сделать, чтобы тот, кто инициировал отмену мог подоставать данные из этих обработчиков с любой глубины, ибо структура чейна промисов может быть совсем любая, и мы не всегда сможем сказать, какие данные наши, какие не наши (например, если в чейне делается несколько запросов, у всех из которых есть обработчики отмены, генерирующие данные, мы отменяем чейн и хотим в результатате операции отмены знать результат отмены конкретного типа запроса). Пока думается, что можно при инициации отмены передавать таблицу Symbol>обработчик, а потом очень аккуратно и обдуманно в обработчиках отмен посылать туда данные, если в сигнале отмены в таблице присутствует некоторый символ.

    Ох, что-то случайно прорвало.


    1. Rulexec
      10.12.2016 01:54

      > Тут пользователь снова жмёт «удалить», очень резко, настолько резко

      Опечатка, жмёт «отменить удаление».


    1. Shifty_Fox
      10.12.2016 02:48

      Эм.
      У вас конкретная специфическая задача — прерывать длинную синхронную функцию (которая внутри асинхронная вся такая, но выглядит синхронно), но в этой функции нет цикла — она просто очень длинная.
      Такая же проблема у вас бы была, если бы у вас была самая обычная функция на много строк, и не мешало бы время от времени проверять, а не стоил ли ее завершить.
      Хорошо, но в случае с await, или тем же .then из es6, у вас уже есть отличная точка входа для абстракции. Вам нужно конкретно под ваш длинный загрузчик из колбеков написать wrap для .then (или await), который будет проверять, необходимо ли прерывать код, и если да — просто не вызывать новый .then (awai).
      Поправьте, если я где-то ошибся.


      1. Rulexec
        10.12.2016 03:51

        Да, так и есть, нужно будет делать что-то вроде .then(checkedForCancel(function(data) { ... })), где checkedForCancel будет возвращать функцию, которая будет проверять какую-нибудь переменную, и только если она всё ещё хорошая, выполнять переданную.


        1. RA_ZeroTech
          10.12.2016 04:12

          мне кажется, что у Вас как раз задача, решение которой заключается в обработке событий…
          если я все правильно понял, то предположу, что задача похожа на «загрузить файл на сервер с возможностью отменить загрузку пока идет процесс»… Если предположение с аналогией верно, то такую задачу (правда на NodeJS), решал именно с помощью обработки событий.


        1. Shifty_Fox
          10.12.2016 14:27

          Ага, только я был сделал checked не внутри then, а поставил бы then внутрь checkedThen. Тогда у вас была бы ровно одна функция checkedThen, и не надо было бы помнить что нужно писать две функции, then и внутри checked, а только одну checkedThen, получилось бы алгоритмически красиво :)


    1. Ryotsuke
      10.12.2016 08:48

      Посмотрите в сторону Observable из RxJS
      Их можно отменить(отписаться), они умеют генерировать несколько событий, а не строго одно — например вернуть промежуточный результат


    1. vladimirgamalian
      10.12.2016 13:24

      Этот недостаток промисов (отсутствие механизма отмены операции до завершения), а также другой — невозможность промежуточных «выстрелов» до полного завершения, решается реактивным подходом. Например в Angular2 предлагается RxJS, чтобы выполнить HTTP запрос с возможностью отмены.


    1. stardust_kid
      11.12.2016 13:05

      А что скажете по поводу метода с cancellation token?


      1. Rulexec
        11.12.2016 22:04

        Посмотрел, очень интересно, явное всегда лучше неявного. Спасибо большое, мне нравится этот подход, посмотрим как-нибудь.


    1. raveclassic
      11.12.2016 18:42

      Посмотрите в сторону генераторов и саг (в частности redux-saga). Достаточно элегантное решение обработки длинных процессов, еще и без самих сайд-эффектов как таковых.


  1. APXEOLOG
    11.12.2016 01:41

    Не понимаю, почему из коробки нет аналога async-waterfall из nodejs и приходится постоянно копипастить его самому. А еще мне не нравится, что выполнение промиса начинается при его создании. Приходится постоянно городить обертки, если работаешь с набором задач


    1. akzhan
      11.12.2016 01:52

      Эм, async поддерживает работу на стороне браузера.


      Async is a utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript. Although originally designed for use with Node.js and installable via npm install --save async, it can also be used directly in the browser.

      https://caolan.github.io/async/


      1. APXEOLOG
        11.12.2016 02:01

        Я понимаю что его можно использовать использовать в браузере, однако я говорю о наличии данной функции из коробки. Не всегда есть возможность подключать лишние js библиотеки


      1. alQlagin
        11.12.2016 09:30

        В ноду async вам тоже нужно устанавливать и подключать. даже если нет возможности поставить пакетным менеджером всегда можно закинуть файл в папку проекта или воспользоваться cdn


  1. num8er
    11.12.2016 11:20

    поправьте пожалуйста:


    return return resolve(1);

    на


    reslove(1);


    1. RA_ZeroTech
      11.12.2016 11:20

      спасибо. исправил


  1. vvadzim
    11.12.2016 22:06

    Есть один лайф-хак у промисов, на статью не танет )

    В конструкторе промиса достаточно очень часто одного параметра — resolve. Исключение, когда reject нужно передавать куда-то как callback — в этом случае лайфхак не удобен, хотя тут на вкус.

    Причина — вызов resolve(Promise.reject(error)) эквивалентен reject(error).

    Возможные минусы:
    — многословнее.
    — создаётся лишний объект. Может быть критично, если промис возвращает ошибку слишком часто.

    Возможные плюсы:
    + многословнее. Паттерн `Promise.reject(` по исходникам как-то легче ищется, и поиском и визуально.
    + меньше идентификаторов — меньше мусора в глазах. Легче реализуются вложенные конструкторы промисов (кейс редкий, но нудный, если уж столкнулся). `new Promise(resolveAaaaa =>… new Promise(resolveBbbbb =>` читается проще, нежели `new Promise((resolveAaaaa, rejectAaaaa) =>… new Promise((resolveBbbbb, resolveBbbbb) =>`. И поиском, опять же, по Promise.reject легче найти все ошибки.
    + к предыдущему — проще паботать с резолверами в каких-то коллекциях, если нужно. Сохранить один-единственный callback и все операции проводить через него приятнее, нежели ныкать объект с полями {resolve, reject}. Кстати, это кейс, при котором количество создаваемых объектов обычно будет меньше, нежели с сохранением reject. При resolve(Promise.reject(..)) дополнительный объект создается только при ошибке, а при сохранении {resolve,reject} — всегда.
    + когда и если будет введён Promise.cancel(), не будет нужды добавлять в код третий параметр, достаточно будет resolve(Promise.cancel(reason)) — опять же, обычно это операция, которая срабатывает редко.


    1. Shannon
      16.12.2016 16:23

      Не плохой вариант


  1. iShatokhin
    11.12.2016 22:07

    Параметр function onSuccess(){} будет вызван в случае успешного выполнения «обещания», function onFail(){} – в случае ошибки. По этой причине следующий код будет работать одинаково:

    Гораздо привычнее и понятнее использовать catch(...).

    Дело не в "привычном" и "непривычном", а в том, что последовательность ловли исключений меняется. Если функция onSuccess выбросит исключение, то в onFail вы это исключение не увидите. Зато увидите в последующем catch.
    Иногда ошибочно кладут внешний callback в последние then и catch одновременно (для результатов или ошибки соответственно), что приводит к двойному вызову callback, если дальше по коду будет синхронно выброшено исключение. Пример такой ошибки — https://github.com/caolan/async/pull/1197


  1. IPri
    12.12.2016 03:41

    Для «loadImage» можно было использовать «fetch», он как раз возвращает промис.

    loadImage на основе fetch
    function loadImage(url){
        return fetch(url)
            .then(response=>response.blob())
            .then(blob=>{
                let img = new Image();
                let imgSRC = URL.createObjectURL(blob);
                img.src = imgSRC;
                return img;
            })
            .catch(err => {
               console.log('loadImage error: ', err, url) 
               throw err;
             });
    }
    
    
    function loadAndDisplayImages(imgList){
        let notLoaded = [];
        let loaded = [];
        let promiseImgs = imgList.map(loadImage);
    
        return promiseImgs.reduce((prev, curr)=>{
            return prev
                .then(() => curr)
                .then(img => {
                   loaded.push(img);
                   document.body.appendChild(img)
                 })
                .catch(err => {
                   notLoaded.push(err);
                   console.log('loadAndDisplayImages error: ', err)
                });
        }, Promise.resolve())
            .then(() => ({'loaded' : loaded, 'notLoaded': notLoaded}));
    }
    
    loadAndDisplayImages([
      'https://hsto.org/getpro/habr/avatars/7ad/1ce/310/7ad1ce31064020bdb79dd73c755ad5ff_small.jpg',
      'https://hsto.org/getpro/habr/avatars/51e/115/c17/51e115c17cbd25fb4adb16c1e3255a32_small.jpg',
      'https://bad_url',
      'https://hsto.org/getpro/habr/avatars/479/a6e/98d/479a6e98d816f8644ff18513cc26a60e_small.png'
    ]).then(console.log);
    


    1. RA_ZeroTech
      12.12.2016 12:31

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


  1. IPri
    12.12.2016 12:59

    Актуальные версии всех основных браузеров (кроме Safari) уже поддерживают — caniuse fetch
    Мне кажется пример с «fetch» с небольшим описанием был бы «изюминкой» вашей статьи. Про «fetch» всего одна статья на Хабре, в отличие от промисов.