Путеводитель по JavaScript Promise для новичков

  • Tutorial
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().

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

Похожие публикации

Комментарии 61

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

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

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

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

            +1
            Промисы-то актуальны, вопрос в том, актуальны ли новые рассказы про промисы для новичков, и уж крайне спорно — называть промисы «новый инструмент» в декабре 2016
              0
              Если честно, про промисы можно вообще всё рассказать буквально за минуту (или за абзац текста и кода), т.к. это просто-напросто иной синтаксис записи обработки событий. Так что меня удивляют большие статьи об этом. Сам научился работать с промисами, тупо проглядев кусок кода, в котором они использовались. Всё понятно и очевидно.
              Имхо, конечно.
                0
                в чем-то Вы правы.
                только кому-то достаточно прочитать документацию, кому-то ее может не хватить, возможно, из-за отсутствия тех примеров, которые будут понятны. статьи же пишут с целью поделиться опытом, который другим может помочь лучше разобраться в той или иной теме. иначе, можно всегда ссылаться на документацию, и не только по этой теме, а вообще.
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    Интересная мысль, надо попробовать. Как в том анекдоте: «Вы знаете, коллега, я третий год объясняю сопромат студентам, и даже сам всё полностью понял!» =).
                    Singapura, у Вас странное написание слов: принято писать не «по-битово» а «побитово»,
                    не «выжал-бы» а «выжал бы».
                    Если же Вы пытались поддеть RA_ZeroTech, то зря: фразы «в чём-то» «кому-то» и «как-то так» «кем-то» пишутся именно так, с дефисом.
                    • НЛО прилетело и опубликовало эту надпись здесь
                      • НЛО прилетело и опубликовало эту надпись здесь
                • НЛО прилетело и опубликовало эту надпись здесь
                0
                В результате вызова myPromise() все равно сработал бы метод then() или catch(). Лучше всего завести сразу привычку — всегда возвращать resolve(...) или reject(...). В будущем это поможет избежать ситуации, когда код будет работать не так, как ожидается.


                Можете пример привести?
                  0
                  Присоединяюсь к вопросу. Никакой необходимости возвращать resolve или reject нет. Напротив, их весьма удобно использовать в качестве коллбеков при «промисификации» всяких коллбековых апи, и никаких return там, разумеется, и в помине нет.
                    0
                    Начав писать ответ на вопрос, понял, что не акцентировал внимание на том, что после вызовов 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);
                    	});
                    


                    В будущем это поможет избежать ситуации, когда код будет работать не так, как ожидается.

                    Этот фраза больше относилась к тем ситуациям, когда забывают возвращать «промис» в своих методах. При этом экспешина не возникает, например:
                    return Promise
                    function myPromise1()
                    {
                    	return new Promise(function (resolve, reject)
                    	{
                    		var err = false;
                    		if (err)
                    			reject('Promise rejected');
                    
                    		resolve('Promise resolved');
                    	});
                    }
                    
                    function myPromise2()
                    {
                    	new Promise(function (resolve, reject)
                    	{
                    		var err = false;
                    		if (err)
                    			return reject('Promise rejected2');
                    
                    		return resolve('Promise resolved2');
                    	});
                    }
                    
                    
                    myPromise1()
                    	.then(function (res)
                    	{
                    		console.log(res);
                    
                    		return myPromise2();
                    	})
                    	.then(function (res)
                    	{
                    		//undefined, хотя долнжо показать "Promise resolved". потому что в myPromise2() не вернули промис
                    		console.log(res);
                    	})
                    	.catch(function(err){
                    		console.log(err);
                    	});
                    

                      +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);
                      	});
                      


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

                      PS. да, есть исключение для этого совета: если эти методы вызываются в качестве колбэков. как правильно заметил Apathetic
                        0
                        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/
                          0
                          http://caniuse.com/#search=Promise
                            0
                            потому что промис — это конечный автомат, вызов reject/resolve меняет состояние, но не завершает выполнение текущей функции.

                            совершенно верно :) именно это я и показал в примере
                        0
                        а как дела обстоят с нативной поддержкой браузерами? уже все подтянулись?
                          0
                          При желании можете использовать, например bluebird.
                            0
                            Скорее даже не при желании, а при возможности — если позволяет «бюджет» (читай — запас по размеру кода), лучше использовать Bluebird, так как он намного быстрее нативных промисов.
                            0
                            Chrome с версии 32, Firefox с 29-ой, Opera с 19-ой, Safari с 7.1, IE который Edge.

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

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

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

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

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

                                    0
                                    Извините, но можно «локально» использовать 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};
                                    
                                      0

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

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

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

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


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

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


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

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

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

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

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

                                            А если вы желаете использовать некую стороннюю библиотеку, которая в свою очередь ожидает window.Promise — это проблема это библиотеки, что она внутри не реализует полифил…
                                            Всегда можно найти библиотеку, которая реализует полифил или самому ее допилить…
                                              0
                                              Именно потому (ИМХО) резонно использовать «обертки» над технологиями…

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

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


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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

                                    Опечатка, жмёт «отменить удаление».
                                      0
                                      Эм.
                                      У вас конкретная специфическая задача — прерывать длинную синхронную функцию (которая внутри асинхронная вся такая, но выглядит синхронно), но в этой функции нет цикла — она просто очень длинная.
                                      Такая же проблема у вас бы была, если бы у вас была самая обычная функция на много строк, и не мешало бы время от времени проверять, а не стоил ли ее завершить.
                                      Хорошо, но в случае с await, или тем же .then из es6, у вас уже есть отличная точка входа для абстракции. Вам нужно конкретно под ваш длинный загрузчик из колбеков написать wrap для .then (или await), который будет проверять, необходимо ли прерывать код, и если да — просто не вызывать новый .then (awai).
                                      Поправьте, если я где-то ошибся.
                                        0
                                        Да, так и есть, нужно будет делать что-то вроде .then(checkedForCancel(function(data) { ... })), где checkedForCancel будет возвращать функцию, которая будет проверять какую-нибудь переменную, и только если она всё ещё хорошая, выполнять переданную.
                                          0
                                          мне кажется, что у Вас как раз задача, решение которой заключается в обработке событий…
                                          если я все правильно понял, то предположу, что задача похожа на «загрузить файл на сервер с возможностью отменить загрузку пока идет процесс»… Если предположение с аналогией верно, то такую задачу (правда на NodeJS), решал именно с помощью обработки событий.
                                            0
                                            Ага, только я был сделал checked не внутри then, а поставил бы then внутрь checkedThen. Тогда у вас была бы ровно одна функция checkedThen, и не надо было бы помнить что нужно писать две функции, then и внутри checked, а только одну checkedThen, получилось бы алгоритмически красиво :)
                                          0
                                          Посмотрите в сторону Observable из RxJS
                                          Их можно отменить(отписаться), они умеют генерировать несколько событий, а не строго одно — например вернуть промежуточный результат
                                            0
                                            Этот недостаток промисов (отсутствие механизма отмены операции до завершения), а также другой — невозможность промежуточных «выстрелов» до полного завершения, решается реактивным подходом. Например в Angular2 предлагается RxJS, чтобы выполнить HTTP запрос с возможностью отмены.
                                              0
                                              А что скажете по поводу метода с cancellation token?
                                                0
                                                Посмотрел, очень интересно, явное всегда лучше неявного. Спасибо большое, мне нравится этот подход, посмотрим как-нибудь.
                                                0
                                                Посмотрите в сторону генераторов и саг (в частности redux-saga). Достаточно элегантное решение обработки длинных процессов, еще и без самих сайд-эффектов как таковых.
                                                0

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

                                                  0

                                                  Эм, 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/

                                                    0

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

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

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


                                                    return return resolve(1);

                                                    на


                                                    reslove(1);
                                                      0
                                                      спасибо. исправил
                                                      0
                                                      Есть один лайф-хак у промисов, на статью не танет )

                                                      В конструкторе промиса достаточно очень часто одного параметра — 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)) — опять же, обычно это операция, которая срабатывает редко.
                                                        0
                                                        Не плохой вариант
                                                        0
                                                        Параметр function onSuccess(){} будет вызван в случае успешного выполнения «обещания», function onFail(){} – в случае ошибки. По этой причине следующий код будет работать одинаково:

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

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

                                                          0
                                                          Для «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);
                                                          

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

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

                                                            Самое читаемое