Как стать автором
Обновить

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

Недавно уже было. Но на мой взгляд эта статья более понятна, без всякой лишней воды.
Да, буквально неделю назад разбирался с промисами, и потерял два дня. :( Толковых материалов мало, в итоге доходил своим умом. Эта статья — очень толкова. Жалко, ее не было две недели назад.
НЛО прилетело и опубликовало эту надпись здесь
  1. Да, функции остаются асинхронными.
  2. Да, поток не будет блокироваться.
  3. И да и нет. В простейших ситуациях промисы действительно больше похожи на удобную обёртку. Однако уже .then() и .catch() дают нам мощный инструмент для управления последовательностью выполнения. А кроме этого есть еще Promise.all() и Promise.race(). Эти методы подробно рассматриваются во второй статье Promises 102(ссылка на оригинал, статья будет переведена в течение ближайшей недели).
НЛО прилетело и опубликовало эту надпись здесь
Почитай про async/await функции. В 7 версии node.js стандартные библиотеки promisify будут и уже async/await работает с флагом harmony.
Те же яйца только в профиль.

Нет. Функция, переданная в then или catch может что-то вернуть или возбудить исключение, т.е. можно использовать return и throw как в обычных функциях. Если функция завершилась нормально, то её результат попадает в следующий then, если было исключение — то exception попадёт в следующий catch. С обычными колбеками так не получится.

Тут я с вами не соглашусь. Допустим, вам предлагают на выбор один из двух проектов:


Пример кода из первого проекта
doSomething(function(responseOne) {  
    doSomethingElse(responseOne, function(responseTwo, err) {
        if (err) { handleError(err); }
        doMoreStuff(responseTwo, function(responseThree, err) {
            if (err) { handleAnotherError(err); }
            doFinalThing(responseThree, function(err) {
                if (err) { handleAnotherError(err); }
                // Выполнено
            }); // конец doFinalThing
        }); // конец doMoreStuff
    }); // конец doSomethingElse
}); // конец doSomething

Пример кода из второго проекта
doSomething()  
  .then(doSomethingElse)
  .catch(handleError)
  .then(doMoreStuff)
  .then(doFinalThing)
  .catch(handleAnotherError)

Который вы выберете?
Единственную ассоциацию, которую вызывает у меня первый пример кода это: "Хочется взять, и переписать!", в то время, как во втором я фрагменте я отчетливо вижу что после чего происходит.

Единственное принципиальное отличие первого от второго заключается в том, что в первом случае кто-то, для устрашения, написал анонимные функции, и выглядит оно страшно, а во втором, используя промисы — таки написал какие-то НЕ анонимные функции. А кажется, что разница на лицо.

Однако, на самом деле, разницы не будет заметно до тех пор, пока не начнет работать async/await, позволяющий тут же вернуть значение из функции, а не обрабатывать его в следующей. Лапшичка (callback hell) никуда не делась, она просто перекрасилась в другой цвет.

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


Другая версия первого примера
function doFinalThingCallback(err) {
    if (err) { handleAnotherError(err); }
    // Выполнено
} // конец doFinalThing

function doMoreStuffCallback(responseThree, err) {
    if (err) { handleAnotherError(err); }
    doFinalThing(responseThree, doFinalThingCallback);
} // конец doMoreStuff

function doSomethingElseCallback(responseTwo, err) {
    if (err) { handleError(err); }
    doMoreStuff(responseTwo, doMoreStuffCallback);
} // конец doSomethingElse

function doSomethingCallback(responseOne) {  
    doSomethingElse(responseOne, doSomethingElseCallback);
} // конец doSomething

doSomething(doSomethingCallback); // конец doSomething

Теперь функции НЕ анонимные. Но последовательность выполнения по прежнему не очевидна. Я бы сформулировал эту проблему так:


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

С промисами ситуация абсолютно обратная


еще раз пример с промисами
doSomething()  
  .then(doSomethingElse)
  .catch(handleError)
  .then(doMoreStuff)
  .then(doFinalThing)
  .catch(handleAnotherError)

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

Для меня самое ценное в промисах это то, что .then можно вызвать когда угодно: до или после завершения выполнения тела, и всё сработает как надо. За счет этого промисы позволяют легко использовать подготовку действий заранее. Например, при открытии какого-то экрана приложения, вы уже можете создать промис, который получит информацию для следующего экрана. А при открытии этого следующего вызвать ему .then(fillPage). И будет неважно, успел запрос выполниться или нет, логика выглядит так же. В этом примере промисы уже не просто выпрямляют цепочку колбеков.
Да. Просто более читабельный вид. Потом был шаг к упрощению при помощи генераторов, а в следующем стандарте вероятно появится async\await.
Все верно, ваш код будет выполняться асинхронно, так же, как при использовании кэлбэков. Промис скорее другой подход, нежели обертка: вы можете предоставить возможность передать кэлбэк, а можете вернуть промис из функции. Второе предпочтительнее, первый подход считается устаревшим. Ну а синхронный и промис стили все-таки не одно и тоже. Совсем «синхронно» это оператор await

Разъясните, await просто останавливает поток выполнения до получения результата? Другими словами, это способ сделать асинхронный код синхронным?

Позвольте процитировать другую статью на хабре — Async/Await в javascript. Взгляд со стороны:


Async/Await в javascript. Взгляд со стороны

Говоря общедоступным языком async/await — это Promise.


Когда вы объявляете функцию как асинхронную, через волшебное слово async, вы говорите, что данная функция возвращает Promise. Каждая вещь которую вы ожидаете внутри этой функции, используя волшебное слово await, то же возвращает Promise.


Из этого можно сделать вывод о том, что async/await являются в большей степени синтаксическим сахаром для промисов. Функция синхронной не становится, поток при этом не блокируется.

Это скорее способ, чтобы асинхронный(но последовательный) код выглядел почти как синхронный линейный код
Отличие только в том, что внутренние асинхронные вызовы нужно предварять await-ом
При этом базовая функция приостановит свое выполнение(но запомнит состояние локальных переменных и стек), и освободит поток для других функций.
Когда же внутренний вызов завершится, исходная функция продолжит свое выполнение с запомненной точки

И самое приятное, что любое исключение(хоть синхронное, хоть асинхронное) может быть одинаково поймано с помощью try..catch
Да. Я Вам даже больше скажу, в известных мне реализациях JavaScript (NodeJS, любой современный браузер) не существует механизмов превращения асинхронного кода в синхронный, равно как не существует и обратного механизма. Существуют обертки разной степени сложности реализации (promises, async&await), но все они не меняют физического способа вызова функции и работы runtime'а.

В NodeJS существуют fibers.

Promises, как шаблон проектирования, предназначен не только для избавления от стилистически некошерных «ёлок», появляющихся при оформлении асинхронных вызовов. Да, они действительно ужасны. Но с асинхронными вызовами появляется риск и посерьёзней, чем сложность восприятия вложенных блоков. И это инверсия контроля. Обещания, это инструмент, созданный для помощи в инвертировании этой инверсии.

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

Вот обещания и предоставляют стандартный паттерн по решению этих задач.

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

Как по мне, то промисы не сильно лучше чем тот же callback hell. Вот await — другое дело. Интересно как с await-ами организовать такое:
Может возникнуть ситуация, когда нам понадобится выполнить несколько промисов параллельно, и продолжать алгоритм только после того, как все промисы будут выполнены.

Какой-нибудь особый параллельный for?

Не согласен, промисы при любом раскладе сильно лучше колбеков. Дождитесь перевода второй статьи Promises 102, где подробно рассказано о том, как их готовить. Так же рекомендую вам ознакомиться со статьёй У нас проблемы с промисами. Автор рассматривает всевозможные проблемы, которые могут возникнуть и возникают при неправильном использовании промисов.
По поводу вашего вопроса, я уже ссылался выше на статью Async/Await в javascript. Взгляд со стороны


Конкретно на ваш вопрос отвечает следующая цитата:


А если мне нужно одновременно получить результат от нескольких вызовов?

Так как мы уже разобрались, что мы имеем дело с Promise. Следовательно можно использовать метод .all() объекта Promise для решения такого рода задач.


async function unicorn() {
  let [rainbow, food] = await Promise.all([getRainbow(), getFood()]);
  return {rainbow, food}
}
Более организованные коллбеки, но всё еще коллбеки.
Вот это вот «Promise.» хорошо бы убрать отовсюду вообще и добавлять по мере надобности новые ключевые слова например fork/join(All)

var a = fork getA(); // выполнение продолжается без ожидания
var b = fork getB();
join; // выполнение приостанавливается пока все fork в этом scope не завершатся
Я видел. Не нравится мне Promise, костыль это, возникший чтобы покрыть недостающие фичи языка.

Очень интересное мнение.
А как бы вы предложили реализовать работу с асинхронными операциями?

Уже есть async/await, осталось перестать вязать его с коллбеками и промисами, подогнать API на замену всяким XMLHttpRequest ну и добавить то, чего не хватает для сложных требований типа параллельно выполнить и подождать выполнения всех:
примеры
var valueFromAnySource = race AsyncHttp.get(url1);
valueFromAnySource = race AsyncHttp.get(url2);
finish;
console.log(valueFromAnySource);

var part1 = fork AsyncHttp.request(...);
var part2 = fork AsyncHttp.request(...);
join;
console.log(part1 + part2)

// если нужно соединить тексты из списка URL в изначальном порядке
// решение без распараллеливания:
var text = ''
for (var url in urls) {
	text += await AsyncHttp.get(url)
}
console.log(text);

// решение с распараллеливанием:
var texts = []
var index = 0;
for (var url in urls) {
	texts[index++] = fork AsyncHttp.get(url)
}
join;
console.log(texts.join(''));

Еще пример:
// в обычном мире:
var p1 = new Promise(
	function(resolve, reject) {
		var xhr = new XMLHttpRequest();
		xhr.open('GET', 'http://google.com');
		xhr.send();
		xhr.onload = function () {
			resolve(this.response);
		}
		xhr.onerror = function() {
			reject(this.statusText);
		}
	}
);
p1.then(
	function(result) {
		console.log(result);
	})
.catch(
	function(err) {
		console.log('error:' + err);
	});

// вымышленный мир без промисов и коллбеков:
try {
	var response = await AsyncHttp.request('GET', 'http://google.com');
	console.log(response.statusCode);
	console.log(response.content);
} catch (err) {
	console.log('error:' + err);
}

Промисы даже без async\await сильно лучше предложенного в первом примере, так как они позволяют думать о данных, а не о контроле выполнения. Второй пример не вымышленный, именно так оно и будет выглядеть с async\await, только вместо придуманного AsyncHttp.request будет вполне реальный fetch. А пример из "реальной жизни" будет выглядеть гораздо лучше если не запихивать промис в переменную, а навешивать then и catch непосредственно, и применить arrow functions:


fetch('http://google.com')
.then(result => console.log(result))
.catch(error => console.log('error:', error))

Пример который по середине, только по людски:


Promise.all(urls.map(url => fetch(url)))
.then(texts => console.log(texts.join('')))

Вобщем, не пытайтесь придумать велосипед, учите лучше современный javascript, все уже за вас придумали.

Жаль, что не продуманы 2 вещи — прогресс и отмена. Особенно отмена, или как минимум прерывание цепочки .then(...).then(...).then(...), предлагают самому в каждом then проверять "флаги". И жаль, что always таки не попал в спецификацию.

и finally тоже

А что вам мешает делать catch и возвращать ошибку?

Да, жаль. Хотя бы отмена настолько нужна, что мне пришлось самому написать свою версию Promise с хорошей отменой (nano-promise). Суть в том, что стандартный промис лишь наблюдатель за асинхронной операцией. Промис не имеет "рычагов" управления для остановки операции. В nano-promise при создании "Обещания" можно вернуть конструктору объект с методом cancel:


function get(url) {
  return new Promise(function (resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      if (req.status == 200)
          resolve(req.response); /* ПРОМИС ВЫПОЛНЕН */
      else
          reject(Error(req.statusText)); /* ПРОМИС ОТКЛОНЁН */
    };

    req.onerror = function() { reject(Error("Network Error")); };
    req.send();

    // Самое главное
    return { cancel: function () {
       req.abort();
    }};
  });

var ps = get('/index.html');
ps.cancel(); // отменяем

Сама отмена не что иное, как вызов reject(Promise.CANCEL_REASON). Ко всему прочему, если есть цепочка из промисов важно корректно отменить всю цепочку с возможными ответвлениями. Т.е. отмена последнего промиса в цепочке порождает отмену до самого начала(при необходимости), а отмена первого спускает отмену (как reject) до самого конца.

Я извиняюсь, если не в тему — но разве не промайсы?
Читается как промис, пишется как промис, зачем промайсы?
Английский язык полон исключений, в этом состоит одна из основных его сложностей. Формально это слово действительно должно читаться так, как написали вы, но на практике именно «промис».
Верно, был не прав. Благодарю.
Блин, как раз имено в сию минуту именно с этими promise вожусь. Правда они реализованы не как-то по своему по ходу.
Тот же define из require.js + стандартный promise из библиотеки — и приложение ломается.
Почему так, еще предстоит выяснить, но чёто чувствую, что сделаю как всегда: аля setInterval с проверкой состояния.
Но может быть сейчас прочитаю эту умную статью и попробую сделать как по уму

Кстати, промисы из разных библиотек прекрасно комбинируются друг с другом.


Promise.resolve(1).then(v => q.resolve(v + 1)).then(v => jQuery.Deferred().resolve(v + 1)).then(v => console.log('done', v))

выведет done 3, как и ожидалось.

Столкнувшись с Promise так и не понял, почему не реализован метод .any() (условно назовём его так), который был по аналогии с .all() выполнил все переданные промисы, но не останавливался при первом reject, а просто сохранял его state.

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

Связанный с вышеуказанным пунктом. Возможно организовать динамическую цепочку промисов. Т.е не явно указанную как в примерах Promise.then().then().then(), но итерацию по списку заданных промисов с последовательным выполнение в указанном порядке, с учётом того что результат resolve (а может и reject) передаётся по цепочке от одного промиса к другому. Можно назвать этот метод .each()

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

Ответ на ваш вопрос можно найти в статье У нас проблемы с промисами или вот под спойлером:


Продвинутая ошибка №3 — промисы против фабрик промисов

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


Сгоряча вы можете написать что-то подобное:


function executeSequentially(promises) {
  var result = Promise.resolve();
  promises.forEach(function (promise) {
    result = result.then(promise);
  });
  return result;
}

К сожалению, пример выше не будет работать так, как задумывалось. Промисы из списка, переданного в executeSequentially(), все равно начнут выполняться параллельно.


Причина в том, что по спецификации промис начинает выполнять заложенную в него логику сразу после создания. Он не будет ждать. Таким образом, не сами промисы, а массив фабрик промисов — это то, что действительно нужно передать в executeSequentially:


function executeSequentially(promiseFactories) {
  var result = Promise.resolve();
  promiseFactories.forEach(function (promiseFactory) {
    result = result.then(promiseFactory);
  });
  return result;
}

Я знаю, вы сейчас думаете: «Кто, черт возьми, этот Java программист, и почему он рассказывает нам о фабриках?». На самом деле фабрика — это простая функция, возвращающая промис:


function myPromiseFactory() {
  return somethingThatCreatesAPromise();
}

Почему этот пример будет работать? А потому, что наша фабрика не создаст промис до тех пор, пока до него не дойдет очередь. Она работает именно как resolveHandler для then().


Посмотрите внимательно на функцию executeSequentially() и мысленно замените ссылку на promiseFactory ее содержимым — сейчас над вашей головой должна радостно вспыхнуть лампочка :)

«фабрика» это отличный термин. Но «лампочка» не загорается, наверное из-за «магии»
Т.к. я не Java программист, то делаю таким образом:
function executeSequentially(promises) {
  var result = Promise.resolve();
  promises.forEach(function (promise) {
    // result = result.then(promise);
    result = result.then(() => promise);
  });
  return result;
}


Через reduce еще изящней
function executeSequentially(promises) {
  return promises.reduce(function(first, next) {
    return first.then(() => next);
  }, Promise.resolve());
}

И оба варианта работают не так, как нужно, прочитайте внимательнее.

Да, вы правы. На самом деле я не использую next как промис. Вместо того чтобы формировать массив промисов, я итерирую массив объектов, по которым нужно сделать последовательные ассинхронные вызовы и в then() создаю анонимную функцию возвращающую промис. Т.е. это как раз фабрика о которой шла речь.

Виноват, что бездумно упростил свой вариант без проверки и выдал как верный.
Спасибо большое, как раз пригодилось

Небольшая головоломка: синхронный код, но параллельные запросы. Как такое может быть? :-)


image


Подсказка: в IE10- запросы идут последовательно.

В IE11- точнее. В IE12+ уже параллельно.

Почему promise API несимметричен относительно деления на первичные функции (в new) и вторичные (во then/catch)?

В первичных надо получать аргументами resolve и reject, и вызывать их явно (проверил на promise 7.1.1 для nodejs — простой return со значением игнорируется, как будто промис незавершён).
Во вторичных надо возвращать значение или генерировать исключение, параметров resolve, reject нет.

Я бы понял логику, или что
1) у всех функций-коллбэков возврат значения работает как resolve с этим значением, а генерация исключения — как reject,
или что
2) все функции-коллбэки имеют три входных параметра — input, resolve, reject,

а лучше — и то, и другое (можно вернуть значение в конце, а можно — вернуть исключение и выйти; а ещё лучше — предусмотреть специальный тип исключения для resolve).

Также нет возможности написать .resolve(значение), и аналогично для reject — тоже было бы значительно удобнее (и извне, как уже обсуждают, и изнутри). (Тогда можно было бы вторым параметром передавать сам объект промиса, для вызовов его методов.)

Или я не вижу каких-то хитростей, которые решают это, и которые можно найти только с заметным опытом их использования?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации