08. Promise in Depth
Last updated
Was this helpful?
Last updated
Was this helpful?
Согласно стандарту, у объекта new Promise(executor)
при создании есть четыре внутренних свойства:
PromiseState
– состояние, вначале «pending».
PromiseResult
– результат, при создании значения нет.
PromiseFulfillReactions
– список функций-обработчиков успешного выполнения.
PromiseRejectReactions
– список функций-обработчиков ошибки.
Когда функция-executor
вызывает reject
или resolve
, то PromiseState
становится resolved
или rejected
, а все функции-обработчики из соответствующего списка перемещаются в специальную системную очередь PromiseJobs
.
Эта очередь автоматически выполняется, когда интерпретатору «нечего делать». Иначе говоря, все функции-обработчики выполнятся асинхронно, одна за другой, по завершении текущего кода. Исключение из этого правила – если resolve
возвращает другой Promise
. Тогда дальнейшее выполнение ожидает его результата (в очередь помещается специальная задача), и функции-обработчики выполняются уже с ним.
Добавляет обработчики в списки один метод: .then(onResolved, onRejected)
. Метод .catch(onRejected)
– всего лишь сокращённая запись .then(null, onRejected)
. Он делает следующее:
Если PromiseState === "pending"
, то есть Promise ещё не выполнен, то обработчики добавляются в соответствующие списки.
Иначе обработчики сразу помещаются в очередь на выполнение.
В .then()
, если один из обработчиков не указан, то Promise добавляет его «от себя», следующим образом:
Для успешного выполнения – функция Identity
, которая выглядит как arg => arg
, то есть возвращает аргумент без изменений.
Для ошибки – функция Thrower
, которая выглядит как arg => throw arg
, то есть генерирует ошибку.
Для следующего примера:
Вид объекта promise после этого:
Все обработчики вешались на тот же Promise
Добавленные нами обработчики f1
, f2
,
Автоматические добавленные обработчики ошибок "Thrower"
.
Все функции из списка обработчиков вызываются с результатом Promise, одна за другой. Никакой передачи результатов между обработчиками в рамках одного Promise нет.
Когда в будущем выполнятся обработчики f1
, f2
, то их результат будет передан в новые Promise по стандартному принципу:
Если вернётся обычное значение (не Promise), новый Promise перейдёт в resolved
с ним.
Если был throw
, то новый Promise перейдёт в состояние "rejected" с ошибкой.
Если вернётся Promise, то используем его результат (он может быть как resolved
, так и rejected
).
This piece of code will leak memory and eventually crash your Node process or browser:
Проблема здесь в том, что не очищается память при вызове предыдущих then
и растет глубина стека.
Unfortunately, just like a regular function using the call stack for those recursive calls, the Promise implementation is abusing the heap memory, not chaining then calls correctly. And that sample should not leak, the Promise implementation should be able to do the equivalent of Tail-Call Optimization and in such a case eliminate frames in the then chain being created.
When you create a Promise, it’s already busy doing its thing. I mean this:
In the console, you will see before
, hello
, after
in that order. So the Promise is eager to call its implementation.
Eager is less general than lazy because it sets restrictions: you cannot reuse eager primitives means that you are restricted from doing that. But you can use lazy primitives one or multiple times, they don’t put any restriction on how many times you can reuse them.
One of the design decisions for Promises was to make them resolve earliest at the end of the current event loop, in order to facilitate solving race conditions with multiple promises created together. It means this code:
Will show before
, after
, 42
in the console.
As a practical result, you can convert from synchronous to Promise, but cannot convert from Promise to synchronous. That’s just an artificial restriction, because callbacks would be able to convert from sync to callback then from callback to sync.