Comments 50
- Да, функции остаются асинхронными.
- Да, поток не будет блокироваться.
- И да и нет. В простейших ситуациях промисы действительно больше похожи на удобную обёртку. Однако уже
.then()
и.catch()
дают нам мощный инструмент для управления последовательностью выполнения. А кроме этого есть ещеPromise.all()
иPromise.race()
. Эти методы подробно рассматриваются во второй статье Promises 102(ссылка на оригинал, статья будет переведена в течение ближайшей недели).
Те же яйца только в профиль.
Нет. Функция, переданная в 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)
Функции могут быть анонимными или именными, сути дела это не меняет — в одном месте кода находится полная последовательность выполнения. Представьте то время, которое вы НЕ потратите с промисами, разбирая чужой код...
Разъясните, await
просто останавливает поток выполнения до получения результата? Другими словами, это способ сделать асинхронный код синхронным?
Позвольте процитировать другую статью на хабре — Async/Await в javascript. Взгляд со стороны:
Говоря общедоступным языком async/await — это Promise.
Когда вы объявляете функцию как асинхронную, через волшебное слово async, вы говорите, что данная функция возвращает Promise. Каждая вещь которую вы ожидаете внутри этой функции, используя волшебное слово await, то же возвращает Promise.
Из этого можно сделать вывод о том, что async/await являются в большей степени синтаксическим сахаром для промисов. Функция синхронной не становится, поток при этом не блокируется.
Отличие только в том, что внутренние асинхронные вызовы нужно предварять await-ом
При этом базовая функция приостановит свое выполнение(но запомнит состояние локальных переменных и стек), и освободит поток для других функций.
Когда же внутренний вызов завершится, исходная функция продолжит свое выполнение с запомненной точки
И самое приятное, что любое исключение(хоть синхронное, хоть асинхронное) может быть одинаково поймано с помощью try..catch
Чем же страшна инверсия контроля? На практике асинхронные вызовы будут происходить к чужому коду, и когда вы передаёте ему функцию обратного вызова, у вас не только нет никакого знания, когда она будет вызвана, но и нет никаких гарантий, что она будет вызвана вообще, или, если будет вызвана, то будет вызвана один раз. В случае же вызова, опять же нет никаких гарантий с какими параметрами это произойдёт. Также не стоит забывать, что делая асинхронный вызов вы попадаете в блоки из которых вам не будут выбрасываться исключения и нужно предусмотреть их генерацию при обработке результата.
Вот обещания и предоставляют стандартный паттерн по решению этих задач.
Есть ещё один плюс у промисов: если у вас операция по какой-то причине выполнится дважды или трижды (даже не могу сходу придумать пример), то традиционный колбэк сработает тоже дважды или трижды, промис же резолвится и вызывает свой колбэк только один раз, уже разрезолвенный промис ничего не делает.
Может возникнуть ситуация, когда нам понадобится выполнить несколько промисов параллельно, и продолжать алгоритм только после того, как все промисы будут выполнены.
Какой-нибудь особый параллельный 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 не завершатся
Очень интересное мнение.
А как бы вы предложили реализовать работу с асинхронными операциями?
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))
Жаль, что не продуманы 2 вещи — прогресс и отмена. Особенно отмена, или как минимум прерывание цепочки .then(...).then(...).then(...)
, предлагают самому в каждом then
проверять "флаги". И жаль, что always
таки не попал в спецификацию.
А что вам мешает делать 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) до самого конца.
Тот же 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
, как и ожидалось.
Подобный метод может использовать например при загрузке информации с нескольких источников, один или несколько из которых могут быть недоступны по каким либо причинам. Но логика работы при этом не должна нарушаться, просто не добавляются все элементы. Сейчас я просто вызываю последовательно методы загрузки и обрабатываю полученные данные в каждом resolve вместо одного общего.
Связанный с вышеуказанным пунктом. Возможно организовать динамическую цепочку промисов. Т.е не явно указанную как в примерах Promise.then().then().then(), но итерацию по списку заданных промисов с последовательным выполнение в указанном порядке, с учётом того что результат resolve (а может и reject) передаётся по цепочке от одного промиса к другому. Можно назвать этот метод .each()
Я понимаю, что подобный функционал можно реализовать вне промисов, но почему то хочется чтобы он был внутри.
Ответ на ваш вопрос можно найти в статье У нас проблемы с промисами или вот под спойлером:
Допустим, вы хотите выполнить серию промисов один за другим, последовательно. Вы хотите что-то вроде 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
ее содержимым — сейчас над вашей головой должна радостно вспыхнуть лампочка :)
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());
}
И оба варианта работают не так, как нужно, прочитайте внимательнее.
Виноват, что бездумно упростил свой вариант без проверки и выдал как верный.
Небольшая головоломка: синхронный код, но параллельные запросы. Как такое может быть? :-)
Подсказка: в IE10- запросы идут последовательно.
В первичных надо получать аргументами resolve и reject, и вызывать их явно (проверил на promise 7.1.1 для nodejs — простой return со значением игнорируется, как будто промис незавершён).
Во вторичных надо возвращать значение или генерировать исключение, параметров resolve, reject нет.
Я бы понял логику, или что
1) у всех функций-коллбэков возврат значения работает как resolve с этим значением, а генерация исключения — как reject,
или что
2) все функции-коллбэки имеют три входных параметра — input, resolve, reject,
а лучше — и то, и другое (можно вернуть значение в конце, а можно — вернуть исключение и выйти; а ещё лучше — предусмотреть специальный тип исключения для resolve).
Также нет возможности написать .resolve(значение), и аналогично для reject — тоже было бы значительно удобнее (и извне, как уже обсуждают, и изнутри). (Тогда можно было бы вторым параметром передавать сам объект промиса, для вызовов его методов.)
Или я не вижу каких-то хитростей, которые решают это, и которые можно найти только с заметным опытом их использования?
Promises 101