Самое важное про промисы

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

0. Что такое промис?

На собеседовании можно сказать, что промис — это экземляр класса Promise. А дальше описать, как этот класс устроен. Для того, чтобы описать какой-то класс, достаточно описать его внутреннее и внешнее устройство.

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

  1. Статус может быть pending, fulfilled и rejected.
  2. Изначально промис может находиться в любом из трех статусов.
  3. Промис может изменить статус не более одного раза (то есть один раз или ни разу).
  4. Можно перейти из статуса pending в fulfilled или из статуса pending в rejected, других вариантов нет.
  5. В состоянии pending промис не хранит никакое значение. В состоянии fulfilled он хранит value. В состоянии rejected хранит reason.
  6. Напрямую (например, обратившись к какому-то полю объекта промиса) получить value или reason нельзя.

Снаружи у промиса доступны: конструктор и три метода then/catch/finally, а также несколько статических методов (например, Promise.all).

  1. Основной метод — then. С помощью него мы можем «подписаться» на событие изменения статуса промиса.
  2. Метод then принимает два аргумента-колбэка. Первый попадает в набор функций для выполнения в случае успеха, а второй — в набор на случай неудачи.
  3. В тот момент, когда поменяется статус промиса, вызовутся либо все подписчики из первого набора, либо все подписчики из второго набора. Подробнее см. п. 2.
  4. Методы catch и finally фактически являются обертками на then и служат только для написания более лаконичного кода

1. Методы промисов выполняются синхронно!

Рассмотрим код:

Promise.resolve(1) .then(x => x + 1) .catch(x => x + 2) .then(x => x + 3)

В этом коде создается 4 промиса! Все они создаются синхронно!

После выполнения этого кода есть один промис в состоянии fulfilled (потому что Promise.resolve, если туда передать не thenable объект, всегда возвращает fulfilled промис).

И есть еще три промиса, находящихся в состоянии pending. Методы then/catch/finally всегда возвращают промис в сотоянии pending. Даже если промис, на котором они вызываются, находится в завершенном состоянии.

2. В какой момент выполняется колбэк then?

Код внутри then никогда не выполняется синхронно!

Рассмотрим код:

p.then(cb)

Если p находится в состоянии pending, то в момент изменения статуса промиса p с pending на fulfilled функция cb отправится в очередь микротасков. Когда осовободится колстек и когда дойдет очередь до этой таски, мы достанем ее, положим на колстек и выполним.

Менее известный факт: даже если p находится в состоянии fullfilled, функция cb все равно не выполнится сразу. Она отправится в очередь микротасков. Но она не выполнится синхронно. Сначала она попадает в очередь, далее мы достаем из очереди все, что там лежало до нее, и только потом достаем ее.

3. Определяем, в какой колбэк мы попали

Рассмотрим код, где на promise1 мы подписываемся через метод then, а на promise2 через метод catch.

const result1 = promise1.then(cb1, cb2); const result2 = promise2.catch(cb2);

От состояния promise1/promise2 зависит только то, в какой коллбэк мы пападем. Состояние промиса result1/result2 (который возвращается then или catch) не зависит от состояния promise1/promise2!

  1. Если promise1 fulfilled, то мы выполняем cb1 (далее см. п. 5)
  2. Если promise1 rejected, то мы выполняем cb2 (далее см. п. 5)
  3. Если promise2 rejected, то мы выполняем cb2 (далее см. п. 5)
  4. Если promise2 fulfilled, то сb1 и cb2 не выполняются

Далее в п. 5 мы увидим, как определяется статус промиса в зависимости от колбэков cb1 и cb2. Но что происходит в последнем случае, когда колбэки вообще не вызываются?

4. Что будет, если передать в then не функцию?

Рассмотрим код

promise1.then(val1, val2) promise2.catch(val2)

Пусть в метод then промиса promise1 мы передали не функцию (а массив, строку, число, другой промис или что-то еще). Кстати, если мы вызвали then с одним аргументом, то это то же самое, что передать вместо val2 undefined.

В этом случае в зависимости от аргумента это значение, которое не функция, заменяется на функцию.

  1. val1 заменяется на x => x
  2. val2 заменяется на x => { throw x }

Например, код

Promise.reject(5).then(x => x + 1)

эквивалентен коду

Promise.reject(5).then(x => x + 1, x => { throw x })

То же самое происходит с catch. Так как мы указываем только второй колбэк (формально он первый и единственный, но фактически это аналог второго колбэка then), то первый мы не указываем вообще. Можно сказать, что val1 для catch это undefined. А это значит, что этот колбэк заменяется на x => x.

Рассмотрим код

const result = promise.catch(cb)

В случае, когда промис promise находится в статусе fulfilled со значаением value, он возвращает новый промис result, который тоже находится в статусе fulfilled со значением value.

5. Как определить статус возвращаемого промиса?

Посмотрим еще раз на код:

const result1 = promise1.then(cb1, cb2); const result2 = promise2.catch(cb2);

После того как мы попали в cb1 или cb2 нам уж не важно как мы туда попали! Состояние promise1/promise2 вообще никак не влияет на состояние result1/result2. Единственное, что влияет — что происходит в колбэке cb1/cb2.

Еще раз напомню, если cb1/cb2 не передан или является не функцией, то происходит автоматическая замена на соответствующую функцию (см. п. 4).

После этого состояние result1/result2 определяется исключительно поведением внутри колбэка, который мы вызвали (как мы выбрали этот колбэк — см п. 3).

С точки зрения алгоритма ниже все эти ситуации рассматриваются одинаково

const result = promise.then(cb); const result = promise.then(×××, cb); const result = promise.catch(cb);

Изначально result всегда находится в состоянии pending, но дальше:

  1. Если в колбэке cb возвращается обычное значение value (не промис) или не возвращается ничего (то есть фактически value равно undefined), то промис result становится fulfilled со значением value.
  2. Если в колбэке cb бросается значение reason (throw reason), то промис result становится rejected со значением reason.
  3. Если в колбэке cb возвращается промис, то промис result получает то же самое значение и тот же самый статус, что этого возвращаемого промиса внутри cb.

6. Зачем нужен метод catch?

p1.then(cb1).catch(cb2); // #1 p1.then(cb1, cb2); // #2

Эти две строчки похожи, но работают по-разному! Если вы хотите обработать обе ситуации — успешное и неуспешное завершение — используйте всегда второй вариант.

Метод catch следует использовать только в двух случаях:

  1. Когда вообще не требуется обработка позитивного сценария и нужно выполнить колбэк только в случае реджекта.
  2. Когда нужно одним колбэком обработать ошибку из нескольких мест. В примере #1 cb2 обрабатывает случай режджекта p1 и ошибку в cb1. В примере #2 cb2 обрабатывает только случай режджекта p1.

7. Конструктор Promise работает синхронно

В п. 2 я начал с того, что then/catch/finally всегда возвращают промис в состоянии pending. Конструктор Promise может возвращать промис, который сразу находится в терминальном состоянии (fulfilled или rejected).

console.log(1); const promise = new Promise(resolve => { console.log(2); resolve("hello"); console.log(3); }); console.log(4); console.log(promise);

Цифры 1, 2, 3, 4 выведутся по порядку. Промис promise в момент вызова resolve меняет статус. То еще до вывода числа 3 он станет fulfilled. Если мы его выедем сразу после промиса в консоль, то увидим его значение. Если бы он становился fulfilled как-то асинхронно, то есть проходя через очередь тасок, то в синхронном коде мы бы увидели pending.

Избранное из MDN

Также рекомендую прочитать 4 статьи на MDN: про промисы в целом, про методы then и catch и про конструктор. Ниже я собрал самую важную информацию из этих разделов.