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

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

НЛО прилетело и опубликовало эту надпись здесь
Спасибо, сейчас перенесу код в статью. У вас JS отключен?
НЛО прилетело и опубликовало эту надпись здесь
Спасибо, оформление даже лучше получилось чем через gist.
НЛО прилетело и опубликовало эту надпись здесь
Да, будет работать, эти промисы совместимы с нативными

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


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(...);
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации