При решении задач про промисы советую подглядывать в эту шпаргалку. Тут в формате разоблачения мифов и ответов на вопросы я описал основные ситуации, которые происходят с промисами.
На собеседовании можно сказать, что промис — это экземляр класса Promise
. А дальше описать, как этот класс устроен. Для того, чтобы описать какой-то класс, достаточно описать его внутреннее и внешнее устройство.
Внутри промиса хранятся: статус промиса, значение и два набора функций-подписчиков (первый набор на случай успешного завершения и второй на случай неудачи).
pending
, fulfilled
и rejected
.pending
в fulfilled
или из статуса pending
в rejected
, других вариантов нет.pending
промис не хранит никакое значение. В состоянии fulfilled
он хранит value
. В состоянии rejected
хранит reason
.value
или reason
нельзя.Снаружи у промиса доступны: конструктор и три метода then
/catch
/finally
, а также несколько статических методов (например, Promise.all
).
then
. С помощью него мы можем «подписаться» на событие изменения статуса промиса.then
принимает два аргумента-колбэка. Первый попадает в набор функций для выполнения в случае успеха, а второй — в набор на случай неудачи.catch
и finally
фактически являются обертками на then
и служат только для написания более лаконичного кодаРассмотрим код:
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
. Даже если промис, на котором они вызываются, находится в завершенном состоянии.
Код внутри then
никогда не выполняется синхронно!
Рассмотрим код:
p.then(cb)
Если p находится в состоянии pending
, то в момент изменения статуса промиса p с pending на fulfilled
функция cb
отправится в очередь микротасков. Когда осовободится колстек и когда дойдет очередь до этой таски, мы достанем ее, положим на колстек и выполним.
Менее известный факт: даже если p находится в состоянии fullfilled
, функция cb
все равно не выполнится сразу. Она отправится в очередь микротасков. Но она не выполнится синхронно. Сначала она попадает в очередь, далее мы достаем из очереди все, что там лежало до нее, и только потом достаем ее.
Рассмотрим код, где на promise1
мы подписываемся через метод then
, а на promise2
через метод catch
.
const result1 = promise1.then(cb1, cb2); const result2 = promise2.catch(cb2);
От состояния promise1/promise2 зависит только то, в какой коллбэк мы пападем. Состояние промиса result1
/result2
(который возвращается then
или catch
) не зависит от состояния promise1
/promise2
!
promise1
fulfilled, то мы выполняем cb1
(далее см. п. 5)promise1
rejected, то мы выполняем cb2
(далее см. п. 5)promise2
rejected, то мы выполняем cb2
(далее см. п. 5)promise2
fulfilled, то сb1
и cb2
не выполняютсяДалее в п. 5 мы увидим, как определяется статус промиса в зависимости от колбэков cb1
и cb2
. Но что происходит в последнем случае, когда колбэки вообще не вызываются?
Рассмотрим код
promise1.then(val1, val2) promise2.catch(val2)
Пусть в метод then
промиса promise1
мы передали не функцию (а массив, строку, число, другой промис или что-то еще). Кстати, если мы вызвали then с одним аргументом, то это то же самое, что передать вместо val2
undefined
.
В этом случае в зависимости от аргумента это значение, которое не функция, заменяется на функцию.
val1
заменяется на x => x
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
.
Посмотрим еще раз на код:
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
, но дальше:
cb
возвращается обычное значение value
(не промис) или не возвращается ничего (то есть фактически value
равно undefined
), то промис result
становится fulfilled
со значением value
.cb
бросается значение reason
(throw reason
), то промис result
становится rejected
со значением reason
.cb
возвращается промис, то промис result
получает то же самое значение и тот же самый статус, что этого возвращаемого промиса внутри cb
.p1.then(cb1).catch(cb2); // #1 p1.then(cb1, cb2); // #2
Эти две строчки похожи, но работают по-разному! Если вы хотите обработать обе ситуации — успешное и неуспешное завершение — используйте всегда второй вариант.
Метод catch
следует использовать только в двух случаях:
cb2
обрабатывает случай режджекта p1
и ошибку в cb1
. В примере #2 cb2
обрабатывает только случай режджекта p1
.В п. 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
.
Также рекомендую прочитать 4 статьи на MDN: про промисы в целом, про методы then
и catch
и про конструктор. Ниже я собрал самую важную информацию из этих разделов.