Pull to refresh

Comments 20

UFO just landed and posted this here
Спасибо, сейчас перенесу код в статью. У вас JS отключен?
UFO just landed and posted this here
Спасибо, оформление даже лучше получилось чем через gist.
UFO just landed and posted this here
Да, будет работать, эти промисы совместимы с нативными

Можно, но не совсем прямо. Например конструкция


async function myFunc() { ... }

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

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


function semiAsyncFunction() {
   return new Promise((resolve, reject) => {
     // синхронные исключения здесь тоже зарежектят промис
   });
}
Спасибо за комментарий. Предполагается, что потребителям возвращается результат `Promise.try`. Почему-то был уверен, что синхронная ошибка не будет отклонена в коде приведенном вами. Тогда в этом методе и правда нет смысла, поменял текст статьи. Ещё раз спасибо!
Все-таки чаще асинхронные функции возвращают промис полученный от другой функции, а не создают его.

Поэтому я бы в качестве замены для Promise.try(...) рассматривал комбинацию Promise.resolve().then(...)

Что-то не то с названием. Мне кажется, что когда пишут "магия внутри", то в статье будет про то, как это внутри работает (а там довольно интересные дела внутри с точки зрения оптимизации происходят), а не вольный пересказ api reference.

Для некоторых из указанных штук, если неплохие альтернативы в нативном async/await


1) Promise.protype.finally(). Здесь все очевидно


try {
  doSomething()
}
catch(e) {}
finally {
  cleanup()
}

2) Promise.any. В стандарте есть похожий Promise.race. У него отличается поведение в случае ошибки, нет AggregationError, но для типичной задачи "берем данные либо по сети, либо из кеша" — работает неплохо:


const result = await Promise.race([tryNetwork(), tryCache(), timeout()])

3) Promise.mapSeries заменяется на обычный for-of цикл


for (const i of [10, 20, 30]) {
   await doRequest(i);
}

4) В дополнение к своему комменту выше про Promise.try замечу, что с async-функциями, даже это не нужно. Асинхронные функции не выкидывают синхронных исключений по определению (что логично, в общем-то).


5) Promise.bind() в async/await коде не нужен, можно просто обойтись переменными:


const user = await fetchUser();
const orders = await fetchOrders(user);
const processed = await checkProcessedOrders(user, orders);

В цепочках then значения user и orders приходилось бы как-то передавать, а здесь все выглядит естественно.


Аналогично с Promise.tap методом, теперь нет проблем вставить вызов посередине


const user = await fetchUser();
await delay(300);
const orders = await fetchOrders(user);

6) Отмену промисов уже завезли в fetch API. Будет доступна с следующем релизе Хрома (66), пока можно поиграться в канарейке.


P.S. ни в коем случае не принижаю полезность bluebird, но если вы решили использовать его только по одной из 6 причин показанных выше, то сперва стоит посмотреть на нативные возможности

P.P.S фича с опциональным перехватом исключений по типу или другому предикату — огонь! Такого даже в Typescript нет. Но реальные use-case для развесистой обработки ошибок по типу встречаются нечасто, обычно достаточно пары if-ов. Но подход красивый, не спорю.

Спасибо, отличное дополнение к статье.

2) Все таки `Promise.race` отличается и поэтому имеет другое применение. Он ведь перейдет в состояние rejected как только любой из переданных промисов перейдет в rejected. Поэтому наиболее частое применение `Promise.race` это конкурирование запроса с каким-либо событием, например, с таймаутом. С другой стороны, если у нас есть несколько источников информации и мы хотим дождаться ответа от любого из них и не получать ошибку до тех пор пока все источники не вернут ошибку, то будем использовать `Promise.any`. Надеюсь не запутал :)

6) С bluebird мы можем любой промис сделать отменяемым + если операция позволяет, отменить и ее

P.S. Использую bluebird в основном из-за map, any и скорости

С bluebird мы можем любой промис сделать отменяемым + если операция позволяет, отменить и ее

С отменяемостью в bluebird неоднозначно.


Во-первых promise.cancel() не отменит операцию, если у нее будет второй консьюмер


var result = fetch(...);

var first = result.then(...);
var second = result.then(...);

first.cancel(); // ничего не произойдет

Вторая проблема, что при отмене промиса, он так и зависнет в неопределенном состоянии. finally вызовутся, а then/catch нет.


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

Давайте разбираться вместе.


Включим отмену:


Promise.config({ cancellation: true });

Для задержки воспользуемся этим методом:


function delay(ms) {
  return new Promise((resolve, reject, onCancel) => {
    const timer = setTimeout(() =>  {
       console.log('timer fired');
       resolve();
    }, ms);

    onCancel(() => { 
      console.log('timer cancelled');
      clearTimeout(timer);
    });
  });
}

Увидим timer fired, A и B:


const source = delay(1000);

const consumerA = source.then(() => console.log(`A`));
const consumerB = source.then(() => console.log(`B`));

Увидим timer cancelled:


const source = delay(1000);

const consumerA = source.then(() => console.log(`A`));
const consumerB = source.then(() => console.log(`B`));

source.cancel();

Увидим timer fired и B:


const source = delay(1000);

const consumerA = source.then(() => console.log(`A`));
const consumerB = source.then(() => console.log(`B`));

consumerA.cancel();

Увидим timer cancelled:


const source = delay(1000);

const consumerA = source.then(() => console.log(`A`));
const consumerB = source.then(() => console.log(`B`));

consumerA.cancel();
consumerB.cancel();

И такое поведение является желаемым в большинстве случаев. У библиотеки нет причин полагать, что вызвав cancel на потребителе вы хотите полностью отменить операцию для всех потребителей. Она делает умнее, отслеживая количество активных потребителей и если их больше нет — распространяет отмену вверх по цепочке.


Важно, что каждый вызов then или catch возвращает новый промис связанный с исходным. Поэтому consumerA не равен consumerB и не равен source. И поэтому увидим timer fired и A:


const source = delay(1000);

const consumerA = source.then(() => console.log(`A`));
const consumerB = source.then(() => console.log(`B`));
const consumerС = consumerB.then(() => console.log(`С`));

consumerС.cancel();

Насчёт fetch. Это не отмена промиса, а отмена нижележащей операции, в данном случае, сетевого запроса. Поэтому промис перейдёт в состояние rejected. Как вы правильно заметили, отмена bluebird-промиса не вызывает ни then, ни catch, что и является желательным поведением при отмене. Пользуясь этой фичей вы вообще не хотите ничего знать о том, как завершиться операция. Отмена через bluebird позволяет отменять именно промисы, а также нижележащие операции, если они такое поддерживают (таймеры, i/o с потоками и т.д.).


Это никак не принижает, а только дополняет, нововведения в fetch, если вы всё таки осмелитесь взять bluebird на клиента. Если нужно чтобы потребители получили уведомление о том, что промис уже ждать не надо, то это другой паттерн — либо timeout либо race.

а зачем вы много где написали свой delay, если уже есть Promise.delay (который делает то же самое)

Спасибо. Думал об этом при написании. В итоге решил оставить как есть для единообразия примеров с delay и delayAndReject. Так как такой код отработает как надо:


Promise.resolve(42).delay(1000).then(...);

А такой вызовет catch сразу:


Promise.reject(new Error('boom')).delay(1000).catch(...);
Sign up to leave a comment.

Articles