Pull to refresh

Comments 61

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

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

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

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

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


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

раз вызвали 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
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/
потому что промис — это конечный автомат, вызов reject/resolve меняет состояние, но не завершает выполнение текущей функции.

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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


return return resolve(1);

на


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

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

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

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

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