Автор: dSalieri
Версия ECMAScript, используемая в объяснении: Draft ECMA-262 / June 24, 2022
Версия WHATWG, используемая в объяснении: Living Standard - 22 August 2022
Последнее изменение документа: 13.12.2022
Оглавление:
- Часть 1: Оболочка promise, составляющие и механизм разрешения
- Часть 2: Методы promise, взаимодействие механизмов promise и его методов
- Часть 3: Создание задачи, ее планирование и выполнение
- Часть 4: События
unhandledrejection
иrejectionhandled
- Часть 5: Конструкторные методы Promise.resolve и Promise.reject
- Часть 6: Демонстрация Promise.all
- Список ссылок данного документа
That was and will continue seriously 48696c5g665g725c
as you see.
А ложь оставьте слабакам,
Для них, забвенье лучше правды,
Для нас, ложь - корень зла!
dSalieri(c)
Что такое promise
? Это экземпляр конструктора Promise. Для чего? Ну, во-первых для ликвидации старых конструкций основанных на callback
, которые усложняли код и делали его слабочитаемым, a во-вторых для отложенного выполнения соответствующего кода, который запускается в определенный момент времени на ответ определенного кода в promise
.
Итак для того чтобы создать экземпляр promise
, нам нужен конструктор и это Promise. Давайте взглянем на его алгоритм.
Алгоритм: Promise ( executor )
1. If NewTarget is undefined, throw a TypeError exception.
2. If IsCallable(executor) is false, throw a TypeError exception.
3. Let promise be ? OrdinaryCreateFromConstructor(NewTarget, "%Promise.prototype%", « [[PromiseState]], [[PromiseResult]], [[PromiseFulfillReactions]], [[PromiseRejectReactions]], [[PromiseIsHandled]] »).
4. Set promise.[[PromiseState]] to pending.
5. Set promise.[[PromiseFulfillReactions]] to a new empty List.
6. Set promise.[[PromiseRejectReactions]] to a new empty List.
7. Set promise.[[PromiseIsHandled]] to false.
8. Let resolvingFunctions be CreateResolvingFunctions(promise).
9. Let completion be Completion(Call(executor, undefined, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)).
10. If completion is an abrupt completion, then
a. Perform ? Call(resolvingFunctions.[[Reject]], undefined, « completion.[[Value]] »).
11. Return promise.
Объяснение: Алгоритм создает экземпляр своего конструктора Promise, при создании экземпляра необходимо передать аргумент, который значится в алгоритме как
executor
. В ходе выполнения этого алгоритма будут созданы функции разрешения (шаг 8) для экземпляра Promise, также последующим действием будет вызовexecutor
(шаг 9) с передачей ему аргументов, первый[[Resolve]]
, второй[[Reject]]
оба они достаются из предыдущего шага (шаг 8). В итоге последним шагом возвращается экземпляр Promise.Учитывая что экземпляр Promise - не простой объект ему присущи скрытые поля (некоторые из них доступны в консоли разработчика для отслеживания), эти поля описаны в спецификации (таблица Internal Slots of Promise Instances).
Вот эта таблица в переводе:
Внутренний слот Тип Описание [[PromiseState]]
pending, fulfilled или rejected Определяет, как promise
будет реагировать на входящие вызовы своего метода then.[[PromiseResult]]
Значение языка ECMAScript Значение, с которым promise
было выполнено fulfilled или отклонено rejected, если таковое имеется. Имеет смысл только в том случае, если[[PromiseState]]
не имеет значение pending.[[PromiseFulfillReactions]]
Список PromiseReaction Записей Записи, подлежащие обработке, когда/если promise
переходит из состояния pending в состояние fulfilled.[[PromiseRejectReactions]]
Список PromiseReaction Записей Записи, подлежащие обработке, когда/если promise
переходит из состояния pending в состояние rejected.[[PromiseIsHandled]]
Boolean Указывает, имел ли promise
когда-либо обработчик выполнения или отклонения; используется для отслеживания необработанных отклонений.Ну и мое объяснение по каждому полю, думаю лишним не будет, итак:
[[PromiseState]]
- отвечает за состояниеpromise
, оно бывает трех видов:pending
,fulfilled
,rejected
(это поле можно отследить в консолиchrome
).[[PromiseResult]]
- отвечает за результатpromise
, то есть когда[[PromiseState]]
меняет свое состояние, то данное поле получает значение, которое потом можно использовать через определенные интерфейсы; например это интерфейсthen
(это поле можно отследить в консолиchrome
).[[PromiseFulfillReactions]]
- отвечает за так называемые реакции/действия PromiseReaction, это поле активно употребляется для того чтобы связатьpromise
, который еще имеет статус поля[[PromiseState]]
равнымpending
, реакциями, чтобы в дальнейшем, когдаpromise
получит в поле[[PromiseState]]
значениеfulfilled
- использовать их для вызова обработчиков, которые находятся в данном поле в специальных записях PromiseReaction.[[PromiseRejectReactions]]
- отвечает за так называемые реакции/действия PromiseReaction, это поле активно употребляется для того чтобы связатьpromise
, который еще имеет статус поля[[PromiseState]]
равнымpending
, реакциями, чтобы в дальнейшем, когдаpromise
получит в поле[[PromiseState]]
значениеrejected
- использовать их для вызова обработчиков, которые находятся в данном поле в специальных записях PromiseReaction.[[PromiseIsHandled]]
- отвечает за статусpromise
, это поле предназначено для отслеживания ошибок созданныхpromise
. Как только мы пользуемся методамиthen
,catch
,finally
тоpromise
получает в это поле[[PromiseIsHandled]]
значениеtrue
. Имеет силу в событиях:unhandledrejection
иrejectionhandled
- они описаны в спецификацииwhatwg
.
Давайте посмотрим на практический пример:
/// Определение функции executor
let executor = (resolve, reject) => {
/// Тело функции executor может быть любым
/// Для более простой демонстрации здесь вызывается resolve в setTimeout
/// В resolve передается значение "ок"
setTimeout(() => resolve("ок"), 3000);
};
/// Создание promise
let p = new Promise(executor);
Если вы выполните этот код и посмотрите что показывает объект p
, вы увидите поле [[PromiseState]]
равное pending
и поле [[PromiseResult]]
равное значению undefined
. Но через 3000мс
, объект p
поменяет [[PromiseState]]
на значение fulfilled
, а [[PromiseResult]]
на значение "ок"
Обратите внимание мы использовали resolve
аргумент в качестве функции. Но вы скорее всего задаетесь вопросом, почему resolve
стала функцией ведь мы ее не определяли. Дело все в CreateResolvingFunctions, этот алгоритм создает разрешающие функции [[Resolve]]
и [[Reject]]
. Разрешающие функции создаются алгоритмом, который создает структуру Record {[[Resolve]], [[Reject]]}
.
Алгоритм: CreateResolvingFunctions ( promise )
1. Let alreadyResolved be the Record { [[Value]]: false }.
2. Let stepsResolve be the algorithm steps defined in Promise Resolve Functions.
3. Let lengthResolve be the number of non-optional parameters of the function definition in Promise Resolve Functions.
4. Let resolve be CreateBuiltinFunction(stepsResolve, lengthResolve, "", « [[Promise]], [[AlreadyResolved]] »).
5. Set resolve.[[Promise]] to promise.
6. Set resolve.[[AlreadyResolved]] to alreadyResolved.
7. Let stepsReject be the algorithm steps defined in Promise Reject Functions.
8. Let lengthReject be the number of non-optional parameters of the function definition in Promise Reject Functions.
9. Let reject be CreateBuiltinFunction(stepsReject, lengthReject, "", « [[Promise]], [[AlreadyResolved]] »).
10. Set reject.[[Promise]] to promise.
11. Set reject.[[AlreadyResolved]] to alreadyResolved.
12. Return the Record { [[Resolve]]: resolve, [[Reject]]: reject }.
Объяснение: Цель этого алгоритма создание специальных функций, которые будут управлять состоянием
promise
. Эти функции обычно называют разрешающими. Алгоритм в особом объяснении не нуждается кроме парочки примечаний. В рамках этого алгоритма будут созданы две функции и помещены в запись видаRecord {[[Resolve]], [[Reject]]}
, каждая из этих функций будет иметь поля[[Promise]]
и[[AlreadyResolved]]
. Поле[[AlreadyResolved]]
будет изначально инициализированно объектом вида{[[Value]]: false}
, так как значение этого поля будет общим для обоих разрешающих функций.Вы наверное много где слышали следующие слова: "Мы не можем переразрешить
promise
, назначив ему новый результат" либо "Нельзя изменить состояниеpromise
сrejected
наfulfilled
". Так вот для всего вот этого при создании разрешающих функций пристыковываются поля[[Promise]]
и[[AlreadyResolved]]
.[[Promise]]
нужен нам для того чтобы знать с каким объектомpromise
мы работаем при его разрешении, а[[AlreadyResolved]]
нам необходимо для того чтобы прекратить выполнение разрешающей функции если она была ранее разрешена и именно поэтому обе разрешающие функции владеют одним и тем же объектом{[[Value]]}
.
Теперь давайте рассмотрим алгоритм resolve
функции, которую мы использовали в примере, передав в качестве аргумента "ок"
. Данное рассмотрение даст вам представление о том из чего состоит resolve
функция
Алгоритм: Promise Resolve Functions
Когда происходит вызов разрешающей функции используется параметр
resolution
Условно в коде:resolve(resolution)
1. Let F be the active function object.
2. Assert: F has a [[Promise]] internal slot whose value is an Object.
3. Let promise be F.[[Promise]].
4. Let alreadyResolved be F.[[AlreadyResolved]].
5. If alreadyResolved.[[Value]] is true, return undefined.
6. Set alreadyResolved.[[Value]] to true.
7. If SameValue(resolution, promise) is true, then
a. Let selfResolutionError be a newly created TypeError object.
b. Perform RejectPromise(promise, selfResolutionError).
c. Return undefined.
8. If Type(resolution) is not Object, then
a. Perform FulfillPromise(promise, resolution).
b. Return undefined.
9. Let then be Completion(Get(resolution, "then")).
10. If then is an abrupt completion, then
a. Perform RejectPromise(promise, then.[[Value]]).
b. Return undefined.
11. Let thenAction be then.[[Value]].
12. If IsCallable(thenAction) is false, then
a. Perform FulfillPromise(promise, resolution).
b. Return undefined.
13. Let thenJobCallback be HostMakeJobCallback(thenAction).
14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback).
15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
16. Return undefined.
Объяснение: Данный алгоритм разрешает
promise
и если уpromise
список реакций[[PromiseFulfillReactions]]
не пустой, тогда используя каждую реакцию PromiseReaction создается задача, которая ставится в очередь планировщику задач. Что касается самого алгоритма, он имеет не один случай завершения поэтому вот список:
Если
[[AlreadyResolved]]
разрешающей функции имеет значениеtrue
- вернуть значениеundefined
Если этот шаг не выполняется то следом идет шаг, который устанавливает
[[AlreadyResolved]]
в значениеtrue
.Если
resolution
и[[Promise]]
это одно и то же значение тогда выполнить алгоритм RejectPromise, передав в качестве аргументов[[Promise]]
и объект ошибки, который создан тут же, после вернуть значениеundefined
.Если
resolution
не значение типаObject
, тогда выполнить алгоритм FulfillPromise, передав в него в качестве аргументов[[Promise]]
иresolution
, после вернуть значениеundefined
.Если Completion является abrupt completion при выполнении Get с аргументами:
resolution
,"then"
- тогда выполнить алгоритм RejectPromise, передав в качестве аргументов:[[Promise]]
и[[Value]]
из abrupt completion, после вернутьundefined
.Этот шаг достаточно сложен в понимании, не углубляясь далеко этот этап можно получить когда вы в качестве
resolution
передаете объект, у которого есть свойство с именемthen
и это свойство является геттером, которое кидает ошибку.Если при выполнении Get с аргументами:
resolution
,"then"
- мы не получаем abrupt completion, тогда мы смотрим если результат операции Get с переданными аргументами:resolution
,"then"
- не функция, тогда выполняем алгоритм FulfillPromise с аргументами:[[Promise]]
иresolution
, после возвращаемundefined
.И наконец самый нетривиальный случай:
- Объявить
thenJobCallback
, который примет результат выполнения HostMakeJobCallback с аргументомthenAction
.- Объявить
job
, который примет результат выполнения NewPromiseResolveThenableJob с аргументами:promise
,resolution
,thenJobCallback
.- Выполнить HostEnqueuePromiseJob с аргументами:
job.[[Job]]
,job.[[Realm]]
.Объяснение этого случая пойдет позже, просто запомните что этот случай связан с созданием задачи и постановкой ее в очередь планировщика задач.
Этот случай возникает когда мы пытаемся в качестве
resolution
передать другой объектpromise
.
Ну и для целостности давайте посмотрим на противоположный алгоритм reject
.
Алгоритм: Promise Reject Functions
Когда происходит вызов разрешающей функции используется параметр
reason
Условно в коде:reject(reason)
1. Let F be the active function object.
2. Assert: F has a [[Promise]] internal slot whose value is an Object.
3. Let promise be F.[[Promise]].
4. Let alreadyResolved be F.[[AlreadyResolved]].
5. If alreadyResolved.[[Value]] is true, return undefined.
6. Set alreadyResolved.[[Value]] to true.
7. Perform RejectPromise(promise, reason).
8. Return undefined.
Объяснение: Данный алгоритм разрешает
promise
и если уpromise
список реакций[[PromiseRejectReactions]]
не пустой, тогда используя каждую реакцию PromiseReaction создается задача, которая ставится в очередь планировщику задач. Что касается самого алгоритма то тут все еще проще:
Проверяется поле
[[AlreadyResolved]]
, если оно имеет поле[[Value]]
со значениемtrue
, тогда вернуть значениеundefined
Если этот шаг не выполняется то следом идет шаг, который устанавливает
[[AlreadyResolved]]
в значениеtrue
.Выполнить алгоритм RejectPromise с аргументами
[[Promise]]
иreason
, после вернутьundefined
Итак вы увидели 2 алгоритма, которые выполняются при вызове либо resolve
либо reject
функции как параметры функции executor
при создании экземпляра Promise. Но это еще не все, теперь я предлагаю посмотреть 2 часто встречающиеся операции в этих двух функциях: FulfillPromise и RejectPromise - именно они играют ключевую роль в разрешении promise
.
Алгоритм: FulfillPromise ( promise, value )
1. Assert: The value of promise.[[PromiseState]] is pending.
2. Let reactions be promise.[[PromiseFulfillReactions]].
3. Set promise.[[PromiseResult]] to value.
4. Set promise.[[PromiseFulfillReactions]] to undefined.
5. Set promise.[[PromiseRejectReactions]] to undefined.
6. Set promise.[[PromiseState]] to fulfilled.
7. Perform TriggerPromiseReactions(reactions, value).
8. Return unused.
Алгоритм: RejectPromise ( promise, reason )
1. Assert: The value of promise.[[PromiseState]] is pending.
2. Let reactions be promise.[[PromiseRejectReactions]].
3. Set promise.[[PromiseResult]] to reason.
4. Set promise.[[PromiseFulfillReactions]] to undefined.
5. Set promise.[[PromiseRejectReactions]] to undefined.
6. Set promise.[[PromiseState]] to rejected.
7. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "reject").
8. Perform TriggerPromiseReactions(reactions, reason).
9. Return unused.
Объяснение: Я не буду для каждого алгоритма писать отдельное объяснение так как алгоритмы практически идентичны в своей сути. Есть только одна разница это операция HostPromiseRejectionTracker, которая встречается в алгоритме RejectPromise, эта операция связана с событием
unhandledrejection
.Оба алгоритма выполняются когда поле
promise
[[PromiseState]]
имеет значениеpending
. Затем они устанавливают полеpromise
[[PromiseResult]]
в значение их второго аргумента. После идет обнуление очередей реакций, но перед обнулением каждый из алгоритмов сохраняет свою очередь реакций в переменную для передачи его в алгоритм TriggerPromiseReactions, оба алгоритма обнуляют обе очереди[[PromiseFulfillReactions]]
и[[PromiseRejectReactions]]
. Последней установкой значения будет поле[[PromiseState]]
, для FulfilledPromise оно устанавливается в значениеfulfilled
, а для RejectPromise устанавливается в значениеrejected
. И последняя операция это TriggerPromiseReactions.
Давайте немедленно рассмотрим TriggerPromiseReactions.
Алгоритм: TriggerPromiseReactions ( reactions, argument )
1. For each element reaction of reactions, do
a. Let job be NewPromiseReactionJob(reaction, argument).
b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
2. Return unused.
Объяснение: Этот алгоритм занимается тем что берет список реакций и с каждой из реакций PromiseReaction создает задачу NewPromiseReactionJob, которая после создания задачи планируется в планировщике задач алгоритмом HostEnqueuePromiseJob. Этот алгоритм не выполняется если список реакций PromiseReaction пуст.
Заключение: На этом этапе мы по сути разобрали устройство promise
, при его создании и его разрешении, операции FulfillPromise и RejectPromise являются финальными этапами разрешения promise
, они устанавливают ему разрешенное значение и устанавливают состояние, также если очереди реакций не пусты выполняется операция TriggerPromiseReactions, которая запускает механизм планирования задач связанных с конкретным promise
. Но есть не менее важная часть его устройства это его методы, которые есть у каждого экземпляра: then
, catch
, finally
. Главный из этих методов это then
, он участвует в алгоритмах catch
и finally
. Рассмотрение этих методов полностью закроет дыры в объяснении некоторых полей объекта promise
, так как именно эти алгоритмы являются потребителями его сущностей.
Итак начнем с примера кода:
/// Создаем экземпляр Promise, разрешающая функция которого будет выполнена примерно через 3000мс
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("Done!"), 3000);
})
/// Говорим что хотим выполнить определенную функцию если успешно или неуспешно
promise.then(
(value) => console.log("Задача:", value),
(reason) => console.warn("Причина провала:", reason)
);
Вы наверняка знаете что функции в then
не будут выполнены в потоке синхронного кода, но будут вызваны после того как resolve
выполнится. Но вы собрались здесь чтобы узнать как вызов функции then
и его производных catch
и finally
взаимодействуют с агрегатными узлами promise
.
Поскольку вы читали первую часть то мы можем смело смотреть на алгоритм then
.
Алгоритм: Promise.prototype.then ( onFulfilled, onRejected )
1. Let promise be the this value.
2. If IsPromise(promise) is false, throw a TypeError exception.
3. Let C be ? SpeciesConstructor(promise, %Promise%).
4. Let resultCapability be ? NewPromiseCapability(C).
5. Return PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability).
Объяснение: Данный алгоритм занимается планированием задач функции, которые вы передаете аргументами в этот метод. Но есть два важных шага:
- Создание записи возможности для promise, алгоритм NewPromiseCapability
- Выполнение основного алгоритма
then
- PerformPromiseThen, в который передается дополнительным аргументом запись возможности promise
Сначала рассмотрим как создается запись возможности promise и для чего она нужна.
Алгоритм: NewPromiseCapability ( C )
1. If IsConstructor(C) is false, throw a TypeError exception.
2. NOTE: C is assumed to be a constructor function that supports the parameter conventions of the Promise constructor (see 27.2.3.1).
3. Let promiseCapability be the PromiseCapability Record { [[Promise]]: undefined, [[Resolve]]: undefined, [[Reject]]: undefined }.
4. Let executorClosure be a new Abstract Closure with parameters (resolve, reject) that captures promiseCapability and performs the following steps when called:
a. If promiseCapability.[[Resolve]] is not undefined, throw a TypeError exception.
b. If promiseCapability.[[Reject]] is not undefined, throw a TypeError exception.
c. Set promiseCapability.[[Resolve]] to resolve.
d. Set promiseCapability.[[Reject]] to reject.
e. Return undefined.
5. Let executor be CreateBuiltinFunction(executorClosure, 2, "", « »).
6. Let promise be ? Construct(C, « executor »).
7. If IsCallable(promiseCapability.[[Resolve]]) is false, throw a TypeError exception.
8. If IsCallable(promiseCapability.[[Reject]]) is false, throw a TypeError exception.
9. Set promiseCapability.[[Promise]] to promise.
10. Return promiseCapability.
Объяснение: Главное назначение алгоритма - создать через конструктор переданный в этот алгоритм:
promise
и его ключевые функции, т.е функции разрешения. Результатом будет запись возможности promise.Я часто употребляю положительный или отрицательный. Под этим подразумевается принадлежность к определеному поведению. В сообществе говорят разрешенное и отклоненное, но так как спецификация считает эти операции разрешающими, я сделал акцент именно на этом, чтобы не создавать конфликты понятий.
За положительным я подразумеваю:
- функцию обратного вызова в
then
первым аргументом, которая сработает когдаpromise
будет иметь в поле[[PromiseState]]
значениеfulfilled
,- также я подразумеваю функцию разрешения
resolve
, которая приводитpromise
к результату в поле[[PromiseState]]
значениеfulfilled
- ну и конечно положительный исход это
[[PromiseState]]
равныйfulfilled
За отрицательным я подразумеваю:
- функцию обратного вызова в
then
вторым аргументом, которая сработает когдаpromise
будет иметь в поле[[PromiseState]]
значениеrejected
,- также я подразумеваю функцию разрешения
reject
, которая приводитpromise
к результату в поле[[PromiseState]]
значениеrejected
- ну и конечно отрицательный исход это
[[PromiseState]]
равныйrejected
Любое другое использование этих слов никак не связано с этой ремаркой, за исключением когда явно в объяснении видно что эти слова объясняют противоположность составляющих
promise
по сторонуfulfill
иreject
Интерфейс выглядит следующим образом (данная таблица в спецификации):
Имя поля Значение Смысл [[Promise]]
Object Объект используемый как promise
.[[Resolve]]
Function object Функция, которая используется для разрешения данного promise
.[[Reject]]
Function object Функция, которая используется для отклонения данного promise
.И цель этого интерфейса позволить создавать записи вида:
Promise.resolve().then().then(() => 5).then((data) => { console.log(data); return "data have been received"; });В этой конструкции мы видим 4 экземпляра Promise. Первый создан конструкцией Promise.resolve(), последующие три записями then(). Вот то что сгенерировано записями
then
и использует запись возможности promise, эта запись использует новый экземпляр Promise, а не тот который мы создаем при создании экземпляра Promise записямиnew Promise
илиPromise.resolve()
. Это служебное созданиеpromise
.Что касается алгоритма, то изначально создается запись возможности promise со значениями компонентов равных
undefined
. Затем создается абстрактное замыкание, в котором указано что при его выполнении установить поля[[Resolve]]
и[[Reject]]
в значения, которые соответствуют параметрам этого замыканияresolve
иreject
, соответственно. Дальше создается встроенная внутренняя функция операцией CreateBuiltInFunction, в которую в качестве аргумента передается абстрактное замыкание из предыдущего шага. После, используя операцию Construct, первый аргумент которого берется из аргумента операции NewPromiseCapability, а второй из предыдущего в качестве исполняемой функции - создается новый экземпляр Promise. Также не забываем установить у записи[[Promise]]
значение, которое только что получили. И в завершение, возвращаем нашу запись возможности инициализированную всеми полями.Вы можете сказать - какой-то странный алгоритм зачем-то создает объект
promise
и зачем то раскладывает его функции разрешения в запись вместе с самим объектомpromise
, непонятно! Почему бы не использовать просто конструктор Promise?Ответ на этот вопрос очень простой: Данный алгоритм реализует защитный
executor
, который не позволит вам перезаписать значения[[Resolve]]
и[[Reject]]
, в местах где используется операция NewPromiseCapability. Если бы спецификация пошла бы путем, который следует из того, что используется пользовательскийexecutor
, в этом случае нарушилась бы логика, которая предписывает поведение непереопределяемых[[Resolve]]
и[[Reject]]
.Попробуйте сами вызвать ошибку на шагах
4.a
и4.b
Помимо всего прочего я хочу вам показать наглядную реализацию этого алгоритма, реализованного на
js
Реализация NewPromiseCapability:
function NewPromiseCapability(C) { if (!(C.prototype && C.prototype.constructor === C)) throw TypeError('C is not a constructor'); const record = { '[[Promise]]': undefined, '[[Resolve]]': undefined, '[[Reject]]': undefined, }; const closure = function (resolve, reject) { if (record['[[Resolve]]']) throw TypeError('Resolve function is not undefined'); if (record['[[Reject]]']) throw TypeError('Reject function is not undefined'); record['[[Resolve]]'] = resolve; record['[[Reject]]'] = reject; }; const promise = Reflect.construct(C, [closure]); if (typeof record['[[Resolve]]'] !== 'function') throw TypeError('Resolve function is not callable'); if (typeof record['[[Reject]]'] !== 'function') throw TypeError('Reject function is not callable'); record['[[Promise]]'] = promise; return record; }Попробуйте передать в него конструктор Promise и вы увидите наглядно что является результатом вызова данной операции.
Теперь когда мы разобрались с записью возможности promise, пришло время посмотреть основную часть алгоритма then
.
Алгоритм: PerformPromiseThen ( promise, onFulfilled, onRejected [ , resultCapability ] )
1. Assert: IsPromise(promise) is true.
2. If resultCapability is not present, then
a. Set resultCapability to undefined.
3. If IsCallable(onFulfilled) is false, then
a. Let onFulfilledJobCallback be empty.
4. Else,
a. Let onFulfilledJobCallback be HostMakeJobCallback(onFulfilled).
5. If IsCallable(onRejected) is false, then
a. Let onRejectedJobCallback be empty.
6. Else,
a. Let onRejectedJobCallback be HostMakeJobCallback(onRejected).
7. Let fulfillReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Fulfill, [[Handler]]: onFulfilledJobCallback }.
8. Let rejectReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Reject, [[Handler]]: onRejectedJobCallback }.
9. If promise.[[PromiseState]] is pending, then
a. Append fulfillReaction as the last element of the List that is promise.[[PromiseFulfillReactions]].
b. Append rejectReaction as the last element of the List that is promise.[[PromiseRejectReactions]].
10. Else if promise.[[PromiseState]] is fulfilled, then
a. Let value be promise.[[PromiseResult]].
b. Let fulfillJob be NewPromiseReactionJob(fulfillReaction, value).
c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]).
11. Else,
a. Assert: The value of promise.[[PromiseState]] is rejected.
b. Let reason be promise.[[PromiseResult]].
c. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle").
d. Let rejectJob be NewPromiseReactionJob(rejectReaction, reason).
e. Perform HostEnqueuePromiseJob(rejectJob.[[Job]], rejectJob.[[Realm]]).
12. Set promise.[[PromiseIsHandled]] to true.
13. If resultCapability is undefined, then
a. Return undefined.
14. Else,
a. Return resultCapability.[[Promise]].
Объяснение: В этот алгоритм приходят 3 обязательных аргумента и один необязательный (в нашем случае необязательный аргумент передается). Первый аргумент является
promise
, на который мы навешивалиthen
методы. Второй и третий это функции разрешения в положительную или отрицательную сторону. И последний необязательный аргумент это запись возможности promise.Так как данный алгоритм имеет разные варианты исхода я опишу их все, но перед этим нужно знать, что вне зависимости от того какой исход будет у данного алгоритма, произойдет следующее:
В самом начале если запись возможности promise не предоставляется то переменная, которая за нее отвечает будет иметь значение
undefined
В самом начале если функции обратного вызова не предоставлены то соответствующие переменные будут иметь значение
empty
, в противном случае в соответствующие переменные будут записаны значения, которые получаются после выполнения операции HostMakeJobCallbackВ самом начале будут созданы для положительного и отрицательного разрешения специальные записи - реакции PromiseReaction, которые состоят из полей (таблица также есть в спецификации):
Имя поля Значение Смысл [[Capability]]
Запись PromiseCapability или undefined Возможности promise
для которого эта запись обеспечивает обработчик реакции.[[Type]]
Fulfill или Reject [[Type]]
используется когда[[Handler]]
имеет значение empty чтобы разрешить поведение для конкретного расчетного типа.[[Handler]]
Запись JobCallback или empty Функция, которая должна быть применена к входящему значению и чье возвращаемое значение будет управлять тем что случится по отношению к производному promise
. Если[[Handler]]
имеет значение empty, вместо него будет использована функция которая зависит от значения[[Type]]
.Смысл этой структуры заключается в том что она объясняет, что делать со случаями когда у нас отсутствуют обработчики и также объясняет что делать когда отсутствует запись реакции
promise
.В конце поле
[[PromiseIsHandled]]
получает значениеtrue
В конце если запись возможности promise предоставлена тогда возвращается из этой записи поле
[[Promise]]
, в противном случае (когда запись не предоставляется) возвращается значениеundefined
Варианты исхода:
Если
promise
имеет в поле[[PromiseState]]
значениеpending
, тогда
- добавить в конец полей
[[PromiseFulfillReactions]]
и[[PromiseRejectReactions]]
соответствующие реакции PromiseReaction.Этот случай возникает когда в синхронном коде создается
promise
, но его поле[[PromiseState]]
имеет значениеpending
, из-за чего планировщик задач даже не знает о задачах подготовленных для него, а все эти функции обратного вызова хранятся в структурах названной PromiseReaction. Складирование этих записей происходит в соответствующие контейнеры: для положительных[[PromiseFulfillReactions]]
для отрицательных[[PromiseRejectReactions]]
. Складирование происходит только еслиpromise
имеет состояниеpending
. Чтобы распланировать накопленные реакции вresolve
иreject
есть вложенная операция TriggerPromiseReactions, то есть распланировка происходит только при срабатывании разрешающих функций.Если
promise
имеет в поле[[PromiseState]]
значениеfulfilled
, тогда
- Выполнить алгоритм NewPromiseReactionJob, в который передаются положительная запись реакции PromiseReaction и
[[PromiseResult]]
- Выполнить алгоритм HostEnqueuePromiseJob, в которую в качестве аргумента передается результат предыдущего шага
Этот случай возникает когда
promise
уже был разрешен положительной функцией разрешения. И как следствие он не использует какие-либо списки реакций. Вместо этого он в единичном порядке создает задачу и ставит ее в очередь задач планировщика задач. Если у васpromise
имеет[[PromiseState]]
fulfilled
, а ниже в коде есть записи сthen
, которые должны выполнить функции обратного вызова, это значит что все эти функции будут распланированы в порядке очереди определения их в коде.Иначе
promise
имеет значение поля[[PromiseState]]
равнымrejected
- Если поле
promise
[[PromiseIsHandled]]
имеет значениеfalse
, тогда выполнить алгоритм HostPromiseRejectionTracker с аргументами, первый которыйpromise
, а второй это значение"handle"
- Выполнить алгоритм NewPromiseReactionJob, в который в качестве аргумента передается отрицательная запись реакции PromiseReaction и
[[PromiseResult]]
- Выполнить алгоритм HostEnqueuePromiseJob передав ему как аргумент результат предыдущего шага.
Этот случай возникает когда
promise
был разрешен отрицательной функцией разрешения. Этот случай также не использует списки реакций как и положительный случай. Также происходит единичное планирование задачи и постановки ее в очередь в планировщик задач. Еслиpromise
имеет[[PromiseState]]
rejected
, а ниже по коду есть записиthen
, которые должны выполнить функции обратного вызова, это значит что все эти функции будут распланированы в порядке очереди определения их в коде.Что касается HostPromiseRejectionTracker это операция, которая занимается отслеживанием обработанных отрицательно-разрешенных
promise
. Этот алгоритм описан в спецификацииwhatwg
и он инициирует вызов событияrejectionhandled
. Смотри четвертую главу.
На этом с Promise.prototype.then мы закончим, думаю логика его предельно ясна. Давайте взглянем на два других метода catch
и finally
, мне кажется вы наверняка думали что принцип их работы совершенно иной, но вы удивитесь что находится у них под капотом внутри.
Алгоритм: Promise.prototype.catch ( onRejected )
1. Let promise be the this value.
2. Return ? Invoke(promise, "then", « undefined, onRejected »).
Объяснение: Как вы можете заметить алгоритм настолько мал насколько возможно. Обратите внимание на последнюю операцию
Invoke
, по сути эта запись пытается сделать следующее:
- Взять объект
promise
- У объекта
promise
, найти методthen
- Вызвать метод
then
с контекстомpromise
и аргументамиundefined
иonRejected
По сути это обычный вызов
then
метода, в котором первыйcallback
не имеет значения для вызова, а второй имеет.Пример:
Promise.reject("просто :)").then(undefined, (reason) => console.log("Причина:", reason))
Теперь давайте взглянем на метод finally
.
Алгоритм: Promise.prototype.finally ( onFinally )
1. Let promise be the this value.
2. If Type(promise) is not Object, throw a TypeError exception.
3. Let C be ? SpeciesConstructor(promise, %Promise%).
4. Assert: IsConstructor(C) is true.
5. If IsCallable(onFinally) is false, then
a. Let thenFinally be onFinally.
b. Let catchFinally be onFinally.
6. Else,
a. Let thenFinallyClosure be a new Abstract Closure with parameters (value) that captures onFinally and C and performs the following steps when called:
i. Let result be ? Call(onFinally, undefined).
ii. Let promise be ? PromiseResolve(C, result).
iii. Let returnValue be a new Abstract Closure with no parameters that captures value and performs the following steps when called:
1. Return value.
iv. Let valueThunk be CreateBuiltinFunction(returnValue, 0, "", « »).
v. Return ? Invoke(promise, "then", « valueThunk »).
b. Let thenFinally be CreateBuiltinFunction(thenFinallyClosure, 1, "", « »).
c. Let catchFinallyClosure be a new Abstract Closure with parameters (reason) that captures onFinally and C and performs the following steps when called:
i. Let result be ? Call(onFinally, undefined).
ii. Let promise be ? PromiseResolve(C, result).
iii. Let throwReason be a new Abstract Closure with no parameters that captures reason and performs the following steps when called:
1. Return ThrowCompletion(reason).
iv. Let thrower be CreateBuiltinFunction(throwReason, 0, "", « »).
v. Return ? Invoke(promise, "then", « thrower »).
d. Let catchFinally be CreateBuiltinFunction(catchFinallyClosure, 1, "", « »).
7. Return ? Invoke(promise, "then", « thenFinally, catchFinally »).
Объяснение: Что-ж, это первый алгоритм, в котором просто какое-то несусветное нагромождение abstract closure (по плану объяснение про это должно было быть главой дальше, но придется здесь объяснять).
Abstract closure - это абстрактное замыкание, которое имеет параметры для функции, замкнутые переменные (сохраненные для использования в будущем) и алгоритм, шаги которого должны быть выполнены, при вызове функции, которая будет использовать это абстрактное замыкание как ключевой алгоритм. Кстати говоря синтаксис замыкания в
php
очень сильно напоминает синтаксис абстрактного замыкания вECMAScript
.Теперь когда вы знаете что такое abstract closure приступим к разъяснению алгоритма.
Алгоритм имеет два случая поведения:
- Случай без передачи функции обратного вызова в
finally
- На шаге 5 проверяется аргумент
onFinally
и если его нельзя вызвать как функцию тогда, создаются две переменныеthenFinally
иcatchFinally
в которые записывается одно и тоже значение из параметраonFinally
- Случай с передачей функции обратного вызова в
finally
- На шаге 6 создается два абстрактных замыкания в шагах
a
иc
, после в шагахb
иd
абстрактные замыкания используются как алгоритмы для функций, которые здесь создаются. По окончанию шага 6 мы имеем две переменныеthenFinally
иcatchFinally
с разными функциями.Но это:
- Выполняется независимо от того какой случай выше выиграл
- Вызывается функция
Invoke
, которая вызывает методthen
с контекстомpromise
и двумя функциями обратного вызоваthenFinally
иcatchFinally
(объяснение проInvoke
было в объяснении проcatch
)Так как я считаю, что данный алгоритм достаточно сложен в понимании, я решил написать его реплику на
javascript
, которая работает (соответствуетECMAScript
практически полностью) в 90% случаев одинаково, остальные 10% вы даже не поймете.Но есть оговорки:
- Реализация не претендует на замену действующего метода
finally
- Метод
SpeciesConstructor
не реализован, так как он не играет решающей роли в понимании того как работает этот метод в концепцииpromise
Реплика
finally
:/// можете развлекаться с этим методом как хотите Promise.prototype._finally = function (onFinally) { const promise = this; if (typeof promise !== 'object') throw TypeError('Promise.prototype._finally called on non-object'); let thenFinally, catchFinally; /// На этом месте должен быть SpeciesConstructor, но вы можете его реализовать, дерзайте! const C = Promise; if (typeof onFinally !== 'function') { thenFinally = onFinally; catchFinally = onFinally; } else { thenFinally = (value) => { const result = onFinally(); const promise = Promise.resolve.call(C, result); const valueThunk = () => value; return promise.then(valueThunk); }; catchFinally = (reason) => { const result = onFinally(); const promise = Promise.resolve.call(C, result); const throwReason = () => { throw reason; }; return promise.then(throwReason); }; } return promise.then(thenFinally, catchFinally); };
Заключение: В этой главе вы узнали как работают методы: then
, catch
и finally
. Вы поняли что метод then
является связующим узлом promise
, результат которого зависит от его состояния. Также вы узнали важность записи возможности promise, без которой стало бы невозможно создавать цепочки then
. Дальше вы познакомитесь с этапами создания задачи, планирования и выполнения.
Примечание: В этой главе я расскажу о том как создается задача, как она планируется, также укажу когда она будет исполнена. Мне придется залезть в смежную спецификацию
whatwg
, так как именно она демонстрирует алгоритмыHost
-процедур (ECMAScript
этого не делает). Ко всему прочему я затрону всем небезызвестный event-loop (цикл событий), на котором продемонстрирую в какой момент времени будет задействована задача (микрозадача - терминwhatwg
)Эта глава полностью совместима с другими концепциями такими как
async
илиasync generators
, если вы хотели узнать как они это делают, то вы по адресу.
Данная часть про расширение концепции Realm спецификацией
whatwg
.
- Realm - глобальная среда, в которой запускается сценарий. Как правило на один Realm один window объект. Поэтому два разных window представляют два разных Realm.
- Settings object - объект настроек окружения для указанного Realm, например, когда создается контекст просмотра, то для него устанавливается соответствующий объект настроек.
Контекст просмотра (Browsing Context) - окружение, в котором браузер отображает объекты документа пользователю. Например:
tab
,window
,iframe
- содержат контексты просмотра. Контекст просмотра может быть вложен один в другой, то есть созданиеtab
и открытие в нем сайта, на котором естьiframe
и будет примером в которомtab
содержит родительский контекст просмотра по отношению к контексту просмотраiframe
.Более подробно читайте спецификацию по данному термину.
Есть несколько способов определения нужного для наших задач Realm в тот или иной момент времени, существует несколько концепций определенных
whatwg
(не все из них используются дляpromise
, это больше для полной картины определения Realm):
Entry
Это соответствует скрипту, который является инициатором для других скриптов или функций. Эта концепция часто используется web-разработчиками когда говорят про так называемый "entry-point" или "точка входа" (по-человечески).
Incumbent
Это соответствует самой последней введенной функции или скрипту в стеке или это функция или скрипт, которые изначально запланировали текущий обратный вызов; отлично описывает эту концепцию алгоритм incumbent settings object.
Current
Это соответствует текущему вызову функции. Как правило спецификация
ECMAScript
оперирует именно этой концепцией у себя.Relevant
Каждый объект платформы имеет relevant Realm. При написании алгоритмов часто-используемый объект платформы - это значение ключевого слова
this
. И в зависимости от того как будет вызываться алгоритм будет определяться его relevant Realm (под объектами платформы подразумеваются объекты, которые созданы данной платформой; объекты, которые описаны вECMAScript
сюда не входят).Иногда требуется знать кто был первоисточником, а кто владельцем, как в качестве данных для использования так и в качестве предотвращения ошибок связанных с потенциальной угрозой как для пользователя так и для владельца приложения. Поэтому присутствует столько определений.
На этом все. Вот ссылка на страницу, где вы можете отыскать все эти термины, что я использовал выше (мои пояснения немного отличаются от спецификации). Если есть желание можете подробно изучить все эти интерфейсы самостоятельно, там же есть ссылки на гитхаб об уменьшении влияния концепций
Entry
иIncumbent
.
Итак в спецификации ECMAScript
есть три места где создается задача и ставится в очередь:
-
Положительно разрешающая функция [[Resolve]]
-
TriggerPromiseReactions это может быть достигнуто как через FulfillPromise так и через RejectPromise
-
-
Отрицательно разрешающая функция [[Reject]]
- TriggerPromiseReactions, достигается через RejectPromise
-
В различных алгоритмах, которые вызывают PerformPromiseThen
В каждом из случаев происходят следующие операции, которые участвуют в создании задачи, постановки ее в очередь планировщика и выполнения:
- HostMakeJobCallback - создание специальной записи, которая содержит непосредственно функцию и набор настроек для ее вызова.
- NewPromiseReactionJob/NewPromiseResolveThenableJob - создание записи, которая содержит задачу и ее Realm.
- HostEnqueuePromiseJob - постановка задачи в очередь для ее исполнения планировщиком задач.
- HostCallJobCallback - вызов функции обратного вызова, которая находится внутри кода задачи.
Вот список этих же операций из ECMAScript
, у них нет четких шагов, но есть определенные требования (этот список мы использовать не будем, он здесь для общей картины):
Итак первый алгоритм, который вступает в бой это HostMakeJobCallback, так как нам необходимо создать запись, которая будет хранить функцию обратного вызова и настройки для нее.
Алгоритм: HostMakeJobCallback ( callable )
1. Let incumbent settings be the incumbent settings object.
2. Let active script be the active script.
3. Let script execution context be null.
4. If active script is not null, set script execution context to a new JavaScript execution context, with its Function field set to null, its Realm field set to active script's settings object's Realm, and its ScriptOrModule set to active script's record.
5. Return the JobCallback Record { [[Callback]]: callable, [[HostDefined]]: { [[IncumbentSettings]]: incumbent settings, [[ActiveScriptContext]]: script execution context } }.
Объяснение: Цель алгоритма создать запись с функциональным объектом функции и настройками для него. Но по правде говоря эта операция довольно специфична, так как вещи, которые она делает - довольно редко встречаются на практике в реальном коде, но все таки встречаются, поэтому я не пропущу ее объяснение.
Первый шаг это получение incumbent settings object, этим шагом мы получаем действующие настройки для кода, который будет выполняться позднее как микрозадача. Это необходимо практически в нескольких случаях: это
API
postMessage (в событии получателя он выдаст значение свойстваorigin
, которое будет основываться на этих настройках) и объект навигации Location (применение происходит в: проверке источника попытки навигации, проверке разрешения на загрузку, передаче копии политики контейнера инициатора, передаче флага временной активации, определениеreferrer
в заголовках запроса).Как вы видите эти случаи в меру специфичные, если вы испытываете желание их изучить то вперед :)
Но вопрос по этому шагу остается открытым, какова его цель? А цель его достаточно проста - сохранить этот incumbent settings object, чтобы при вызове тех перечисленных случаев настройки были правильно переданы, как если бы те случаи вызывались бы в синхронном коде, а не из обработчика
promise
, когда активный скрипт отсутствует.Второй шаг это получение структуры script (данная структура отвечает за все что может быть связанно со скриптом, это происходит на уровне приложения, которое с данным скриптом работает; не путайте с элементом script) через операцию active script, впоследствии этот
active script
будет использован как скрипт (структура, а не элемент и не запись скрипта) у которого берется значение поля base URL для создания правильных путей до скриптов когда используется выражение import(). Стоит отметить что active script возвращает у Script Record/Module Record поле[[HostDefined]]
, которое содержит в себе структуру скрипта. Особенность в том что при создании скрипта, а это и есть та самая структура, выполняется операция определенная в ECMAScript как ParseScript, вот именно в эту операцию передается структура скрипта, которая записывается в поле[[HostDefined]]
к Script Record/Module Record.Третий шаг это создание переменной execution context, которая инициализируется значением
null
Четвертый шаг это проверка шага два, если шаг два имел значение
null
, тогда ничего не делать, в противном случае - создать новый execution context и скопировать ключевые поля из результата второго шага в него. Этот шаг играет важную роль когда используется запись import(). Смотрите, функции, которые вы определяете в скриптах как правило привязываются к Script Record/Module Record где вы эту функцию объявили т.к у любой функции есть поле[[ScriptOrModule]]
, в которой хранится Script Record/Module Record/null
. Но некоторые функции имеют в[[ScriptOrModule]]
поле значениеnull
, как правило это встроенные функции в язык и окружение исполнителя, также это обработчики событий контента; то есть:<span onclick="console.log("hello, researcher")">Click here</span>но данное правило не касается обработчиков объявленных через код.
Итак, создание execution context на этом этапе гарантирует что если мы в
promise
в качестве функции обратного вызова положим функцию со значениемnull
поля[[ScriptOrModule]]
, то данный execution context сохранит контекст скрипта когда будет вызываться import() и использует правильный base URL, в противном случае произойдет "разгерметизация" execution context и мы окажемся на execution context, который находится в стеке гораздо глубже и как результат мы получим неверныйURL
запроса к скрипту.Взгляните на примеры из спецификации:
Когда
active script
не равенnull
, тогда в таком случае происходит создание execution context c передачей в негоactive script
Promise.resolve('import(`./example.mjs`)').then(eval);Данный пример хорош тем что демонстрирует обработчик как встроенную функцию, которая имеет
[[ScriptOrModule]]
равноеnull
. Таким образом выполнение функциейeval
строчки'import(`./example.mjs`)'
должно будет опереться на execution context, который специально создавался для таких случаев. Base URL будет извлечен именно из него.На момент написания: Концепция с сохранением дополнительного execution context и настроек из
active script
- не работает. По поведениямchrome
иfirefox
складывается впечатление, что они ее просто не учитывают, так как import() получит другойbase URL
, который будет взят у документа, что противоречит данной концепции.Когда
active script
равенnull
, тогда в таком случае создание execution context, пропускается<button onclick="Promise.resolve('import(`./example.mjs`)').then(eval)">Click me</button>Этот пример прекрасно демонстрирует, как в данном случае функция-обработчик созданная в документе не создаст execution context, такое поведение определили разработчики, чтобы лишить двусмысленности обработчиков, которые определяются не в коде, а как часть значений атрибутов
html
, поскольку активный скрипт на момент вызова обработчика будет отсутствовать - поэтому такое поведение интуитивно понятно так как мы четко можем понять как такой обработчик был определен и как решать пути для наших печально известных import().Пятый шаг это создание записи задачи, в которую помещается функция обратного вызова и объект настроек.
Теперь наступает этап когда нам нужно на основе предыдущего шага создать так называемую задачу, которая впоследствии будет поставлена в очередь микрозадач event-loop. Операции, которые создают эти задачи: NewPromiseReactionJob и NewPromiseResolveThenableJob. Мы их рассмотрим парой так как логика у них одинаковая.
Алгоритм: NewPromiseReactionJob ( reaction, argument )
1. Let job be a new Job Abstract Closure with no parameters that captures reaction and argument and performs the following steps when called:
a. Let promiseCapability be reaction.[[Capability]].
b. Let type be reaction.[[Type]].
c. Let handler be reaction.[[Handler]].
d. If handler is empty, then
i. If type is Fulfill, let handlerResult be NormalCompletion(argument).
ii. Else,
1. Assert: type is Reject.
2. Let handlerResult be ThrowCompletion(argument).
e. Else, let handlerResult be Completion(HostCallJobCallback(handler, undefined, « argument »)).
f. If promiseCapability is undefined, then
i. Assert: handlerResult is not an abrupt completion.
ii. Return empty.
g. Assert: promiseCapability is a PromiseCapability Record.
h. If handlerResult is an abrupt completion, then
i. Return ? Call(promiseCapability.[[Reject]], undefined, « handlerResult.[[Value]] »).
i. Else,
i. Return ? Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »).
2. Let handlerRealm be null.
3. If reaction.[[Handler]] is not empty, then
a. Let getHandlerRealmResult be Completion(GetFunctionRealm(reaction.[[Handler]].[[Callback]])).
b. If getHandlerRealmResult is a normal completion, set handlerRealm to getHandlerRealmResult.[[Value]].
c. Else, set handlerRealm to the current Realm Record.
d. NOTE: handlerRealm is never null unless the handler is undefined. When the handler is a revoked Proxy and no ECMAScript code runs, handlerRealm is used to create error objects.
4. Return the Record { [[Job]]: job, [[Realm]]: handlerRealm }.
и вместе с ним
Алгоритм: NewPromiseResolveThenableJob ( reaction, argument )
1. Let job be a new Job Abstract Closure with no parameters that captures promiseToResolve, thenable, and then and performs the following steps when called:
a. Let resolvingFunctions be CreateResolvingFunctions(promiseToResolve).
b. Let thenCallResult be Completion(HostCallJobCallback(then, thenable, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)).
c. If thenCallResult is an abrupt completion, then
i. Return ? Call(resolvingFunctions.[[Reject]], undefined, « thenCallResult.[[Value]] »).
d. Return ? thenCallResult.
2. Let getThenRealmResult be Completion(GetFunctionRealm(then.[[Callback]])).
3. If getThenRealmResult is a normal completion, let thenRealm be getThenRealmResult.[[Value]].
4. Else, let thenRealm be the current Realm Record.
5. NOTE: thenRealm is never null. When then.[[Callback]] is a revoked Proxy and no code runs, thenRealm is used to create error objects.
6. Return the Record { [[Job]]: job, [[Realm]]: thenRealm }.
Объяснение: Создание записи с задачей и ее Realm, все это понадобится позднее когда интерпретатор приступит к выполнению задачи.
Оба имеют три ключевых этапа:
- Шаг первый, создание abstract closure (объяснение было ранее), которая будет являться кодом задачи, это замыкание свяжет функцию обратного вызова с внутренними интерфейсами
promise
, которые очень важны, когдаpromise
будет разрешаться, без них этот объект всегда будет в состоянииpending
- Набор шагов, которые определяют Realm для функции обратного вызова, знание Realm позволяет работать с конкретным окружением, в котором функция была определена (я еще покажу, важность этого момента).
- Последний шаг, создание специальной записи, в которой будет храниться код задачи (abstract closure) и ее Realm
Вот так все просто. Но мы еще вернемся к этим abstract closure, совсем скоро.
Теперь операция, которая планирует задачи HostEnequeuePromiseJob, но перед ней давайте ознакомимся с event-loop, который даст вам представление о том что это такое.
Ликбез по event-loop
Браузер это как правило многопоточное приложение, но некоторые части его движка выполняются одним потоком. Концепция event-loop представляет поток, в котором в бесконечном цикле выполняются операции, которые обеспечивают функциональность страницы и отзывчивость ее интерфейса. Через эту концепцию проходят наши запросы для выполнения задачи от promise.
Вот упрощенная модель того что делает event-loop каждый цикл:
Ищет самую старую запланированную макрозадачу из очереди макрозадач и выполняет ее
Выполняет все микрозадачи, которые есть (если микрозадачи создают новые микрозадачи, то этот шаг затянется до тех пор пока все микрозадачи не выполнятся)
Если этот цикл event-loop выполнял макрозадачу, то он осуществляет проверку на длительность макрозадачи, если она меньше
50мс
прерывает этот этап, в противном случае сообщает всем связанным с макрозадачей Realms о медленной макрозадаче; эта вещь непосредственно связана сAPI
long tasks.Выполняет операцию обновления содержимого связанного с текущим event-loop и его документами если этот event-loop имеет происхождение window, все не подходящие для обновления документы исключаются из набора шагов ниже
В состав этой операции входят вызовы разных подопераций таких как (в порядке перечисления):
- обновление элементов автофокуса документа,
- обработка размеров документа,
- обработка прокрутки документа,
- обработка медиа-запросов специального API,
- обновление анимации и отправка событий,
- обработка полноэкранного режима,
- обработка восстановления canvas холста,
- вызов функций обратного вызова перед рендерингом страницы, интерфейс requestAnimationFrame,
- запуск обновления наблюдателя пересечений элементов,
- вызов алгоритма об отметке времени покраски
- обновление пользовательского интерфейса на экране
Если текущий цикл event-loop не имеет макрозадачи, имеет пустой стек микрозадач и список документов не пуст, тогда это цикл простоя, применяется интерфейс requestIdleCallback
Если этот event-loop имеет происхождение worker, тогда:
- Если этот event-loop имеет реализацию интерфейса
DedicatedWorkerGlobalScope
и движок реализации считает что необходимо выполнить рендеринг, то:
- Вызывает функцию обратного вызова requestAnimationFrame
- Выполняет обновление рендеринга этого worker, чтобы отобразить текущее состояние
- Если список микрозадач пуст и флаг
closing
имеет значениеtrue
- Уничтожает этот event-loop и прерывает его шаги
Синхронный скрипт по сути своей является макрозадачей, пока он не выполнится обновление страницы не произойдет и event-loop будет висеть над этой задачей до тех пор пока браузер не посчитает, что что-то со страницей не то и предложит закрыть ее (вы наверняка встречались с таким поведением).
Как только макрозадача завершится - наступает очередь микрозадач, они для этого и были предназначены, чтобы выполнять какие-то действия после основной задачи, так вот, концепцияpromise
и основывается на них. Микрозадачи позволяют создать детерминированное поведение, когда мы четко пониманием в какой период они начнут исполнятся после того как они были запланированы.
Итак вы должны понимать, что после макрозадачи идет выполнение всех существующих микрозадач для текущего event-loop, когда event-loop повторяется происходит все тоже самое и так каждый раз (это с учетом того что мы берем в расчет что у нас на каждом цикле event-loop есть макрозадачи и нет циклов простоя).
Цикл:: макрозадача -> микрозадачи -> ... -> начать новую итерацию цикла
Стоит отметить что этапы
макрозадача
имикрозадачи
несут необязательный характер, так как некоторые итерации цикла могут не иметь макрозадач, то же касается и микрозадач. Поэтому итерации цикла могут быть как только с макрозадачами так и только с микрозадачами.Набор скриптов на странице
html
, группируется в одну макрозадачу, скрипты не создают никакие макрозадачи они туда включаются в рамках другой макрозадачи, как правило это задача получения/парсинга html файла. Функции обратного вызова от setTimeout или setInterval тоже планируются, как макрозадачи, это кстати не единственное, что подходит под определение макрозадачи, здесь определен список частоупотребляемых источников задач в модели браузера, а здесь список алгоритмов, которые инкапсулируют эти задачи.Микрозадачи как правило создаются в рамках управления
promise
конструкциями, также существует интерфейс создания микрозадачи вне концепцииpromise
- queueMicrotaskСудя по спецификации requestAnimationFrame и requestIdleCallback не входят ни в список макрозадач ни в список микрозадач, они выполняются как отдельные задачи в рамках event-loop
События, которые вызываются синтетически то есть программно, не являются никакими задачами, а вот события которые происходят от действий пользователя заключаются в макрозадачу. Будьте внимательны с этим тонким нюансом.
Ну и давайте я обозначу все что написал про event-loop псевдокодом на js
Это не реализация, а абстрактное представление модели event-loop (данный псевдокод отражает больше нюансов чем объяснение выше)
/// this = event-loop instance function processEventLoop() { while (true) { let oldestTask = null, taskStartTime = null; /// Этап макрозадачи if(this.hasTaskQueueWithRunnableTask) { let taskQueue = this.taskQueues.chooseTaskQueueInAnImplementationDefinedManner(); taskStartTime = unsafeSharedCurrentTime(); oldestTask = this.taskQueue.takeRunnableTask(); this.taskQueue.removeRunnableTask(); this.currentlyRunningTask = oldestTask; oldestTask.run(); this.currentlyRunningTask = null; } /// Этап микрозадач (() => { if(this.performingAMicrotaskCheckpoint) return; this.performingAMicrotaskCheckpoint = true; while(this.microtaskQueue.length > 0) { let microtask = this.microtaskQueue[0]; this.microtaskQueue.remove(microtask); this.currentlyRunningTask = microtask; microtask.run(); this.currentlyRunningTask = null; } /// EnvironmentSettingsObjects - абстракция которая описывает всевозможные настройки и не имеет конкретного владельца EnvironmentSettingsObjects.forEach((settings) => { /// Операция, которая создает и планирует макрозадачу с выстрелом события unhandledrejection this.notifyAboutRejectedPromises(settings); }); this.performingAMicrotaskCheckpoint = false; })(); let hasARenderingOpportunity = false; let now = unsafeSharedCurrentTime(); /// Доклад о долгой макрозадаче if (oldestTask !== null) { LongReportTask(oldestTask, taskStartTime, now); } if (this instanceof Window) { /// Данный раздел относится к циклу событий окна (window) const docs = GetAllDocumentsForRelevantAgent(); docs.removeDocumentsThatHaveNotRenderOpportunity(); if(docs.length > 0 && this.lastRenderOpportunityTime === true) { hasARenderingOpportunity = true; this.lastRenderOpportunityTime = taskStartTime; } docs.forEach((doc) => { if (document.defaultView === document.defaultView.top) { doc.flushAutofocusCandidates(); } }); docs.forEach((doc) => doc.runTheResizeSteps()); docs.forEach((doc) => doc.runTheScrollSteps()); docs.forEach((doc) => doc.evaluateMediaQueriesAndReportChanges()); docs.forEach((doc) => doc.updateAnimationsAndSendEvents()); docs.forEach((doc) => doc.runTheFullscreenSteps()); docs.forEach((doc) => doc.contextLostSteps()); /// Этап взаимодействует с rAF docs.forEach((doc) => doc.runTheAnimationFrameCallbacks()); /// Этап взаимодействует с IntersectionObserver docs.forEach((doc) => doc.runTheUpdateIntersectionObservations()); docs.forEach((doc) => doc.markPaintTiming()); /// Этап обновления содержимого на экране (в него также входит интеграция ResizeObserver) docs.forEach((doc) => doc.updateTheRenderingUserInterface()); if (this instanceof Window && oldestTask === null && this.microtaskQueue.length === 0 && !hasARenderingOpportunity) { /// Если условие пройдено значит вы добрались до цикла простоя, этот этап взаимодействует с rIC this.lastIdlePeriodStartTime = unsafeSharedCurrentTime(); const computeDeadline = () => { let deadline = this.lastIdlePeriodStartTime + 50; let hasPendingRenders = false; /// SameLoopWindows - это объекты window, которые относятся к текущему event-loop SameLoopWindows.forEach(() => { if (SameLoopWindows.mapOfAnimationFrameCallbacks.length !== 0 || UserAgentBelievesThat_SameLoopWindowsMightHavePendingRenderingUpdates) { hasPendingRenders = true; } let timerCallbackEstimates = gettingTheValues(ToFlatMap(SameLoopWindows.getMapsOfActiveTimers)); timerCallbackEstimates.forEach((timeoutDeadline) => { if(timeoutDeadline < deadline) deadline = timeoutDeadline; }); }); if(hasPendingRenders === true){ let nestRenderDeadline = this.lastRenderOpportunityTime + 1000 / CurrentRefreshRate; if(nextRenderDeadline < deadline) return nextRenderDeadline; } return deadline; }; /// startAnIdlePeriodAlgorithm создает макрозадачу для того чтобы выполнить все запросы на rIC SameLoopWindows.forEach((win) => win.startAnIdlePeriodAlgorithm(computeDeadline())); } } else if (this instanceof WorkerGlobalScope) { /// Данный раздел относится к event-loop работника (worker) if (this instanceof DedicatedWorkerGlobalScope && UserAgentBelievesThatRenderingWouldBeBenefical) { this.runTheAnimationFrameCallbacks(); this.updateTheRenderingOfThatWorker(); } if (this.taskQueue.length === 0 && this.closing === true) { this.destroy(); } } } }
Алгоритм: HostEnequeuePromiseJob ( job, realm )
1. If realm is not null, then let job settings be the settings object for realm. Otherwise, let job settings be null.
2. Queue a microtask on the surrounding agent's event loop to perform the following steps:
1. If job settings is not null, then check if we can run script with job settings. If this returns "do not run" then return.
2. If job settings is not null, then prepare to run script with job settings.
3. Let result be job().
4. If job settings is not null, then clean up after running script with job settings.
5. If result is an abrupt completion, then report the exception given by result.[[Value]].
Объяснение: Постановка микрозадачи в очередь микрозадач. Данный этап выполняется сразу после создания специальной записи, которая содержит задачу и ее Realm (подразумеваются операции NewPromiseReactionJob/NewPromiseResolveThenableJob)
Если параметр
realm
не имеет значениеnull
, тогда
job settings
будет результатом settings object дляrealm
В противном случае
job settings
будет иметь значениеnull
Если
realm
не имеет значениеnull
, запустится Realm авторского кода. Когдаjob
возвращается операцией NewPromiseReactionJob,realm
принадлежит функции-обработчикуpromise
. Когдаjob
возвращается операцией NewPromiseResolveThenableJob,realm
принадлежитthen
функции (речь идет о специфическом случае когда в resolve передают объект, который имеетthen
метод).Если
realm
имеет значениеnull
, то либо нет авторского кода который выполнится либо авторский код гарантированно бросит ошибку. Для первого, автор может не иметь переданным в код для запуска функции-обработчика, как например вpromise.then(null, null)
. Для последнего, это причина отозванногоProxy
, который был передан в качестве функции обратного вызова. В обоих случаях все шаги ниже, которые могли бы использоватьjob settings
- пропускаются.Поставить в очередь микрозадачу в event-loop окружающего его агента (agent), чтобы выполнить следующие шаги:
Что такое agent - концептуально, архитектурно-независимый идеализированный "поток", в котором
JavaScript
код запускается. Такой код может вовлекать множество глобальных объектов, realms, которые могут синхронно обращаться друг к другу и следовательно должны выполняться в одном потоке исполнения. Агент владеет своим собственным event-loop
Если
job settings
не имеет значениеnull
, тогда
- Выполнить проверку можно ли запустить скрипт с аргументом
job settings
, если результат этой проверки"do not return"
прекратить дальнейшее выполнениеЭта проверка проверяет готовность документа функции-обработчика и если он полностью готов, то разрешить дальнейшее выполнение этого алгоритма
Ранее я говорил, что Realm важен, когда мы его сохраняли в операциях NewPromiseReactionJob/NewPromiseResolveThenableJob в специальную запись. Знание Realm функции-обработчика, позволит нам проверить можем ли мы эту функцию запустить, использовав его settings object. Если по итогу проверка проходит неуспешно, то дальнейшие шаги микрозадачи прерываются и наш
promise
остается в состоянииpending
.Такой случай возникает когда, например есть окно открывателя и окно, которое открыватель открыл.
В одном из окон определяют функцию и передают ее любым доступным способом в другое окно. После окно, которое передало функцию закрывается. Теперь в окне, которое получило функцию из другого окна, создают выражение с
then
, в которое вкладывают полученную функцию.Результат: функция не выполняется, так как функция-обработчик считается непригодной из-за того, что ее документ не является полностью активным (фактически полностью активный документ это документ, к которому не просто есть доступ по ссылке, но также этот документ связан с контекстом просмотра и он активен в контексте просмотра).
Поэтому Realm так важен :)
Вот пример который описывает мои слова выше:
Файл: opener.html
/// Это код окна открывателя const opener = window.open("newTab.html"); window.onmessage = function(e){ if(e.data === "close"){ opener.close(); /// Так как команда не моментально закрывает окно, соответственно есть промежуток во времени, когда документ открытого окна считается полностью активным. И поэтому я добавил задержку, чтобы к тому периоду времени документ открытого окна не считался полностью активным. Но нет никаких гарантий, что через этот интервал времени документ открытого окна будет не полностью активным. setTimeout(() => console.log(Promise.resolve().then(logIt).then((data) => console.log(data))), 1000) } }
Файл: newTab.html
/// Это код открытого окна открывателем window.onload = function(){ if(!window.opener) return; window.opener.logIt = logIt; window.opener.postMessage("close", window.location.origin) }; function logIt(){ return "I don't care about the Realm!"; }На момент написания: браузер
firefox
соответствует спецификации и не печатает сообщение, которое возвращает функцияlogIt
, а вотchrome
не соблюдает спецификацию, что соответственно является багом. Аккуратнее!Если
job settings
не имеет значениеnull
, тогда
Выполнить подготовку к запуску скрипта с аргументом
job settings
Подготавливает execution context к выполнению авторского кода
Пусть
result
будет результатом выполненияjob()
job
это abstract closure, которое возвращается операцией NewPromiseReactionJob или NewPromiseResolveThenableJob. Обработчик функцииpromise
, когдаjob
возвращается операцией NewPromiseReactionJob и обработчик функцииthen
когдаjob
возвращается операцией NewPromiseResolveThenableJob - заключены в JobCallback Records.HTML
сохраняет incumbent settings object и JavaScript execution context для active script в HostMakeJobCallback и восстанавливает их в операции HostCallJobCallback.Если
job settings
не имеет значениеnull
, тогда
- Сделать очистку после запуска скрипта с аргументом
job settings
Если
result
это abrupt completion, тогда
- Доложить об исключении, передавая
result.[[Value]]
Как вы поняли этот алгоритм конкретно занимается постановкой микрозадач в очередь микрозадач. Шаги микрозадачи начнут выполнятся только тогда когда наступит время этой микрозадачи, на данном этапе определен алгоритм микрозадач.
После того как мы поставили микрозадачу в очередь микрозадач, осталось ее дождаться, вспомните ликбез по event-loop, там этап выполнения микрозадач наступает после выполнения макрозадачи. Когда момент выполнения микрозадачи наступает выполняется ее код, который определен в алгоритме, который мы только что рассмотрели, шаг job()
запускает abstract closure, который был определен либо в NewPromiseReactionJob либо в NewPromiseResolveThenableJob. Теперь мы рассмотрим шаги этих abstract closure.
Код abstract closure из NewPromiseReactionJob, которая замкнула переменные reaction
, argument
a. Let promiseCapability be reaction.[[Capability]].
b. Let type be reaction.[[Type]].
c. Let handler be reaction.[[Handler]].
d. If handler is empty, then
i. If type is Fulfill, let handlerResult be NormalCompletion(argument).
ii. Else,
1. Assert: type is Reject.
2. Let handlerResult be ThrowCompletion(argument).
e. Else, let handlerResult be Completion(HostCallJobCallback(handler, undefined, « argument »)).
f. If promiseCapability is undefined, then
i. Assert: handlerResult is not an abrupt completion.
ii. Return empty.
g. Assert: promiseCapability is a PromiseCapability Record.
h. If handlerResult is an abrupt completion, then
i. Return ? Call(promiseCapability.[[Reject]], undefined, « handlerResult.[[Value]] »).
i. Else,
i. Return ? Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »).
Объяснение: Итак этот код будет выполнен в рамках event-loop как микрозадача. Так как этот алгоритм имеет не одну точку завершения, чтобы понять что мы получим в конце есть следующие правила, которые присутствуют в алгоритме:
Если
[[Handler]]
имеет значениеempty
, тогда
Если
[[Type]]
имеет значениеFulfill
, тогда
- Выполнить операцию NormalCompletion, передав в него аргумент
argument
, результат сохранить вcallbackResult
В противном случае
[[Type]]
имеет значениеReject
, тогда - Выполнить операцию ThrowCompletion, передав в негоargument
, результат сохранить вcallbackResult
Случай о пустых обработчиках
Такой случай происходит в следующем примере:
let p1 = Promise.resolve("ok"); let p1_then = p1.then(); console.log(p1 === p1_then); /// false /// или let p2 = Promise.reject("oops"); let p2_then = p2.then(); console.log(p2 === p2_then); /// falseВышеобозначенные примеры логически соответствуют таким записям:
Promise.resolve("ok").then((value) => value, undefined); /// и Promise.reject("oops").then(undefined, (reason) => {throw reason});Для напоминания: в
then
могут быть переданы аргументы, которые не являются вызываемыми т.е функции. Аргументы, которые не являются вызываемыми будут обработаны так как если бы аргументов им не передавалосьВ противном случае
[[Handler]]
неempty
, тогда
- Выполнить HostCallJobCallback с аргументами
[[Handler]]
,undefined
,argument
, после обработать получившиеся операцией Completion, результат сохранить вcallbackResult
Этот условный блок предназначен для выявления имеет ли PromiseReaction
[[Handler]]
не пустым, как вы знаете в этом поле хранится функция обратного вызова изthen
. Если пустой - сохраняем значение, которое в зависимости от[[Type]]
является аварийным или нет. В случае если[[Handler]]
у нас функция обратного вызова - тогда выполняем ее и результат сохраняем, предварительно обработав операцией Completion.HostCallJobCallback - операция, которая вызывает функцию обратного вызова из
then
конструкции, которую вы передаете в ответ на удачное или неудачное разрешениеpromise
. Мы рассмотрим ее чуть позже.Если
[[Capability]]
имеет значениеundefined
, тогда
- Вернуть значение
empty
Этот условный блок предназначается для случаев когда мы не хотим и нам не нужно разрешать служебный
promise
. Обычно таких ситуаций не бывает (нет случаев когда создавался бы служебныйpromise
, а потом не разрешался по заключительному решению; ну лично я не видел). Но есть случаи когда не создается этот служебныйpromise
, это хорошо известная всем конструкцияawait
, она возвращает чисто результат, а неpromise
.Если
callbackResult
имеет abrupt completion (аварийное завершение), тогда
- Вернуть результат вызова операции Call, в которую передаются аргументы
[[Reject]]
,undefined
иcallbackResult
В противном случае normal completion (нормальное завершение), тогда
- Вернуть результат вызова операции Call, в которую передаются аргументы
[[Resolve]]
,undefined
иcallbackResult
Этот условный блок показывает, что для разных результатов
callbackResult
вызываются разные разрешающие функции. Также это показывает вам, что служебно созданныйpromise
выражениемthen
также разрешается, но после того как выполнит функцию обратного вызова переданную вthen
.
Код abstract-closure из NewPromiseResolveThenableJob, которая замкнула переменные promiseToResolve
, thenable
, then
:
a. Let resolvingFunctions be CreateResolvingFunctions(promiseToResolve).
b. Let thenCallResult be Completion(HostCallJobCallback(then, thenable, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)).
c. If thenCallResult is an abrupt completion, then
i. Return ? Call(resolvingFunctions.[[Reject]], undefined, « thenCallResult.[[Value]] »).
d. Return ? thenCallResult.
Объяснение: Этот код будет выполнен в рамках event-loop как микрозадача.
Создать разрешающие функции для
promiseToResolve
операцией CreateResolvingFunctions, результат поместить в переменнуюresolvingFunctions
Этот шаг предназначен для того чтобы достать необходимые функции, которые будут в состоянии разрешить наш внешний
promise
Выполнить операцию HostCallJobCallback, с передаваемыми в нее аргументами
then
,thenable
,resolvingFunctions.[[Resolve]]
,resolvingFunctions.[[Reject]]
. Результат оборачивается операцией Completion и помещается в переменнуюthenCallResult
.Если предыдущий шаг это abrupt completion, тогда
- Вернуть результат вызова операции Call, в которую передаются
resolvingFunctions.[[Reject]]
,undefined
,thenCallResult.[[Value]]
Вернуть
thenCallResult
Оба abstract-closure выше делают две ключевые вещи:
- Выполняют функцию обратного вызова, которая им была передана каким-либо образом
- Разрешают
promise
Теперь возвращаемся к операции HostCallJobCallback, так как именно она запускает функцию обратного вызова, которую обычно передают в операцию PerformPromiseThen (это может происходить разными способами: самый явный способ, где конкретно вы передаете функции-обработчики - then
и через неявные способы передачи, где функции-обработчики создаются алгоритмами внутри: ключевое слово await
, асинхронные итераторы, исполнение асинхронных модулей, динамический импорт, асинхронные генераторы).
Алгоритм: HostCallJobCallback ( callback, V, argumentList )
1. Let incumbent settings be callback.[[HostDefined]].[[IncumbentSettings]].
2. Let script execution context be callback.[[HostDefined]].[[ActiveScriptContext]].
3. Prepare to run a callback with incumbent settings.
4. If script execution context is not null, then push script execution context onto the JavaScript execution context stack.
5. Let result be Call(callback.[[Callback]], V, argumentsList).
6. If script execution context is not null, then pop script execution context from the JavaScript execution context stack.
7. Clean up after running a callback with incumbent settings.
8. Return result.
Объяснение: Наконец-то, сквозь неимоверные усилия мы дошли до операции, которая запускает нашу функцию обратного вызова переданную в
then
(либо еще каким-образом, зависит от того где и как вызывается PerformPromiseThen; например дляawait
реализация сама создает функцию и кладет в PerformPromiseThen, но это тема отдельного разговора)
Первый шаг, получение
incumbent settings
, который мы сохраняли на этапе HostMakeJobCallbackВторой шаг, получение
script execution context
, который мы сохраняли на этапе HostMakeJobCallbackТретий шаг, подготовка к запуску функции обратного вызова туда передается как аргумент
incumbent settings
Вы должны помнить подробное объяснение в HostMakeJobCallback, итак эта операция кладет содержимое
incumbent settings
в стек запасныхincumbent settings objects
, чтобы интерфейсы, которые нуждаются в этом объекте могли его спокойно получить при отсутствии активного объекта настроек, тот же postMessage как пример, который я показывал ранее.Четвертый шаг, если
script execution context
не имеет значениеnull
, тогда втолкнуть его в execution context stackКак я говорил ранее
chrome
иfirefox
(может быть и еще какие-то браузеры) по какой-то причине не учитываютactive script
, а по тому этот шаг игнорируется реализациями (хотя не должен)Пятый шаг, вызов операции Call с аргументами
callback.[[Callback]]
,V
,argumentList
. Результат этого шага записывается в переменнуюresult
.Это непосредственный вызов нашей функции обратного вызова
Шестой шаг, если
script execution context
не имеет значенияnull
, тогда вытолкнуть его из execution context stackСедьмой шаг, очистка после запуска функции обратного вызова
Удаляет
incumbent settings
из стека incumbent settings objects, который использовался при подготовке к запуску функции обратного вызоваВосьмой шаг, возврат
result
По окончанию управлениe передается в abstract closure созданное либо операцией NewPromiseReactionJob либо NewPromiseThenableReactionJob, они завершают логическое выполнение нашей микрозадачи с разрешением
promise
в положительный или отрицательный результатНа этом этапе завершается выполнение поставленной микрозадачи, если существуют еще микрозадачи то самая первая в очереди будет вытолкнута из стека микрозадач, а затем исполнена и так пока стек микрозадач не опустеет, после чего наступит макрозадача. Также не стоит забывать что микрозадачи могут создать новые микрозадачи и созданные микрозадачи будут помещены в этот же стек и как следствие из этого выполнение стека микрозадач до полного опустошения.
Ну и чтобы все подытожить - вот схема всех этих действий:
Initial call - начальный вызов, то что начинает процесс связанный с
promise
Awaiting microtask checkpoint - это этап ожидания выполнения микрозадач
Заключение: Эта глава показала вам когда и где начинается планирование микрозадачи, куда она попадает после планирования и что происходит когда наступает ее время исполнения. Бонусом вы увидели механизм event-loop, который показал на каком этапе происходит выполнение этих микрозадач. На этом все.
Ранее я абстрагировался от описания этих событий, так как эти события связаны с Host
-операциями, которые как правило спецификация ECMAScript
не описывает, а только накладывает требования к реализации. Рассматривать в тот момент сразу whatwg
- было бы поспешно, так как отсутствие понимания promise
привело бы к чрезвычайно тяжелому восприятию этих событий. Сейчас же самый благоприятный момент для этого. Приступаем!
Событиe unhandledrejection
. Его точка входа располагается в алгоритме RejectPromise. Шаг который начинает путь к вызову события выглядит так:
If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "reject").
Теперь событие rejectionhandled
. Его точка входа располагается в алгоритме PerformPromiseThen. Шаг который начинает путь к вызову события выглядит так:
If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle").
Обратите внимание, что они оба проверяют внутреннее поле promise
[[PromiseIsHandled]]
, ранее я уже описывал что этот слот означает, но я напомню - этот слот хранит в себе булево значение, которое отражает состояние promise
был ли он обработан или нет (по сути когда вызывается операция PerformPromiseThen, в одном из его шагов он устанавливает promise
полю [[PromiseIsHandled]]
в значение true
, выполнение данной операции считается интерпретатором, что promise
обработан). В данном случае если данный слот содержит значение false
то вызывается операция HostPromiseRejectionTracker, в которую передается текущий promise
и его тип обработки reject
или handle
.
Теперь когда вы знаете откуда идут импульсы, нам важно узнать, что делает операция HostPromiseRejectionTracker.
Алгоритм: HostPromiseRejectionTracker ( promise, operation )
1. Let script be the running script.
2. If script is a classic script and script's muted errors is true, terminate these steps.
3. Let settings object be script's settings object.
4. If operation is "reject",
1. Add promise to settings object's about-to-be-notified rejected promises list.
5. If operation is "handle",
1. If settings object's about-to-be-notified rejected promises list contains promise, then remove promise from that list and return.
2. If settings object's outstanding rejected promises weak set does not contain promise, then return.
3. Remove promise from settings object's outstanding rejected promises weak set.
4. Let global be settings object's global object.
5. Queue a global task on the DOM manipulation task source given global to fire an event named rejectionhandled at global, using PromiseRejectionEvent, with the promise attribute initialized to promise, and the reason attribute initialized to the value of promise's [[PromiseResult]] internal slot.
Объяснение: Эта операция позволяет отслеживать отклоненные
promise
и в зависимости от обстоятельств выдавать то или иное событие.
Первый шаг, получает запущенный скрипт и сохраняет в
script
Этот шаг получает Script Record/Module Record
Второй шаг, проверяет если результат на шаге выше является classic script и его поле muted errors равно значению
true
, прекратить шаги данного алгоритмаТретий шаг, пусть
settings object
будет settings object извлеченный изscript
Четвертый шаг, если
operation
имеет значение"reject"
, тогда
- Добавить
promise
вsettings object
в поле about-to-be-notified rejected promises list.Пятый шаг, если
operation
имеет значение"handle"
, тогда
- Если
settings object
его поле about-to-be-notified rejected promises list содержитpromise
, тогда удалитьpromise
из этого списка и вернуться.- Если
settings object
его поле outstanding rejected promises weak set не содержитpromise
, тогда вернуться.- Удалить
promise
изsettings object
поля outstanding rejected promises weak set.- Пусть
global
будет извлечено из определения global object принадлежащего кsettings object
- Поставить в очередь глобальную задачу, передавая DOM manipulation task source и
global
, чтобы выстрелить событием названнымrejectionhandled
вglobal
, используя интерфейсPromiseRejectionEvent
с атрибутами"promise"
инициализированным в значениеpromise
и"reason"
инициализированным в значение слота[[PromiseResult]]
отpromise
.Если сейчас совсем еще непонятно, что делает эта операция и почему здесь только одна задача, которая выстреливает событием - все нормально.
В этом алгоритме да и вообще есть две коллекции, которые отвечают за отслеживание отклоненных
promise
:
about-to-be-notified rejected promises list
Это обычный список, в который собираются отклоненные
promises
, которые не были обработаны операцией PerformPromiseThen.Используются для
promises
со статусом"reject"
.outstanding rejected promises weak set
Это слабый набор, в который собираются
promises
, которые прошли этап вызова событияunhandledrejection
(событие выстреливает в любом случае даже если нет обработчиков). Слабый набор реализуется для того чтобы оптимизировать процесс контроля как самихpromises
так и памяти. К примеру попадает отклоненныйpromise
и в дальнейшем он не получает обработки, такой объект начинает висеть в памяти, чтобы этого избежать вы удаляете все ссылки на такойpromise
и по причине слабого набора он из него исчезает и не держит лишнюю память. Также при реализации такого набора он может быть ограничен размером, чтобы удалять старые ссылки при добавлении новых.Используется для
promises
со статусом"handle"
.Теперь когда вы знаете что есть две коллекции, можно разъяснить шаги алгоритма выше подробнее.
Итак вы наверное заметили, что 4 шаг берет на себя обработку
"reject"
, а 5 шаг обрабатывает"handle"
.Четвертый шаг:
- Занимается тем что ставит все запросы с
"reject"
в специальный список about-to-be-notified rejected promises list.Но кроме постановки в специальный список больше ничего нет. Как же так спросите вы? Всему свое время. Я вам позже покажу как появляется событие связанное с
"reject"
, просто на данном этапе запомните что скрипт помнит какиеpromise
были необработаны.Что же касается пятого шага, то тут все достаточно понятно:
Сначала происходит проверка about-to-be-notified rejected promises list, это делается для того чтобы отклоненный
promise
, который на одной и той же итерации event-loop получил обработку, не оповещался (оповещение не сработает ни по одному из событий).Дальше происходит проверка outstanding rejected promises weak set, если в нем нет
promise
, который мы передаем в данную операцию, тогда выходим из него, но если в нем естьpromise
тогда продолжаем. Смысл этой операции проверить данный набор на наличиеpromise
, так как есть случаи когда обработка происходит операцией PerformPromiseThen перед самым носом:let p = Promise.reject("oops"); p.then(undefined, () => {});Отсутствие данной проверки привело бы к дальнейшему выполнению и вызову события
rejectionhandled
. Детали по данному примеру будут раскрыты чуть больше когда мы рассмотрим весь механизм.Дальше мы удаляем из outstanding rejected promises weak set наш
promise
, так как считается что он получает обработку, и за этим он больше не находится в наборе отклоненных.Ну и в конце мы получаем из нашего скрипта глобальный объект и используем его в планировании глобальной задачи, при вызове которой произойдет выстрел событием
rejectionhandled
.По этому алгоритму все.
Но есть часть того что прошла незамеченной мимо нас давайте к ней обратимся.
Вызов событий связанных с отклоненными promise
тесно связан с event-loop, на этапе выполнения микрозадач происходит вызов функции notify about rejected promises (данная операция вызывается после того как все микрозадачи выполнились) для всех environment settings object связанных с текущим event-loop.
Именно notify about rejected promises создает и ставит в очередь макрозадачу, которая при выполнении вызывает событие unhandledrejection
. Важно понимать что эта операция выполняется после макроздачи и после выполнения всех микрозадач, но в операции, которая отвечает за выполнение микрозадач.
Алгоритм: notify about rejected promises (как аргумент принимает environment settings object, который в алгоритме будет иметь имя settings object
)
1. Let list be a copy of settings object's about-to-be-notified rejected promises list.
2. If list is empty, return.
3. Clear settings object's about-to-be-notified rejected promises list.
4. Let global be settings object's global object.
5. Queue a global task on the DOM manipulation task source given global to run the following substep:
1. For each promise p in list:
1. If p's [[PromiseIsHandled]] internal slot is true, continue to the next iteration of the loop.
2. Let notHandled be the result of firing an event named unhandledrejection at global, using PromiseRejectionEvent, with the cancelable attribute initialized to true, the promise attribute initialized to p, and the reason attribute initialized to the value of p's [[PromiseResult]] internal slot.
3. If notHandled is false, then the promise rejection is handled. Otherwise, the promise rejection is not handled.
4. If p's [[PromiseIsHandled]] internal slot is false, add p to settings object's outstanding rejected promises weak set.
Объяснение: Этот алгоритм создает и ставит в очередь макрозадачу, которая начнет выполнятся в следующей event-loop итерации (с учетом приоритизации макрозадач)
Первый шаг, создать копию списка в
list
из about-to-be-notified rejected promises list, что принадлежитsettings object
Второй шаг, если
list
пустой, вернутьсяТретий шаг, очистить about-to-be-notified rejected promises list, что принадлежит
settings object
Четвертый шаг, получить в
global
значение из global object, что принадлежитsettings object
Пятый шаг, поставить в очередь глобальную задачу c аргументами DOM manipulation task source и
global
, которая при выполнении запустит следующие шаги:
Для каждого
promise
(p
) вlist
(это цикл):
Если
p.[[PromiseIsHandled]]
имеет значениеtrue
, то переходим к следующей итерации цикла (не event-loop)Пусть
notHandled
будет результатом выстрела события названнымunhandledrejection
на объектеglobal
, также используяPromiseRejectionEvent
с атрибутамиcancelable
установленным в значениеtrue
,promise
установленным в значениеp
иreason
установленным в значениеp.[[PromiseResult]]
Если
notHandled
имеет значениеfalse
, тогда отклонениеpromise
являетсяhandled
. В противном случае отклонениеpromise
являетсяnot handled
Дело в том, что выстрел событием возвращает булево значение и этот шаг на него опирается. Также этот шаг сообщает о
handled
иnot handled
- эти концепции используются браузером, чтобы сообщать ему об ошибкахpromises
.Если этот шаг доходит до
not handled
это означает, что браузер в консоли может выбросить ошибку (обычно вы видите ошибку отклоненного не обработанногоpromise
). Чтобы ее не получить, то есть чтобы алгоритм дошел доhandled
, вам нужно в обработчике событияunhandledrejection
написатьevent.preventDefault()
Если
p.[[PromiseIsHandled]]
имеет значениеfalse
, тогда добавитьp
в outstanding rejected promises weak set, что принадлежитsettings object
В конце хотелось бы отметить вот что:
Взгляните, алгоритм планирует макрозадачу - это означает лишь одно, что инструкции, которые находятся внутри этой макрозадачи будут выполнены на следующей итерации event-loop и в соответствии с приоритизацией макрозадач. Следовательно событие
unhandledrejection
появляется всегда на следующей итерации event-loop (следующий не подразумевает буквально следующий, имеется ввиду на одной из следующих итераций, так как приоритизация макрозадач имеет непосредственное влияние)
Итак я вам показал все составляющие, которые приводят к двум событиям: unhandledrejection
и rejectionhandled
.
Давайте пройдемся по примерам с комментариями по ним:
Перед примерами сделаем соглашение, что у нас есть два обработчика написанных в коде следующим образом:
window.addEventListener("unhandledrejection", (event) => console.log(event.type)) window.addEventListener("rejectionhandled", (event) => console.log(event.type))
Пример 1:
let p = Promise.reject("oops");Данный пример запускает HostPromiseRejectionTracker с аргументом
"reject"
, что приводит нас к добавлениюpromise
в about-to-be-notified rejected promises list. Когда все микрозадачи будут исполнены произойдет шаг notify about rejected promises, который создаст и запланирует макрозадачу. На текущей итерации event-loop больше ничего не происходит, однако на одной из следующих итераций event-loop произойдет выполнение запланированной макрозадачи. Когда такое случится - произойдет выстрел событиемunhandledrejection
. Если есть зарегистрированный слушатель с функцией обратного вызова, тогда функция обратного вызова сработает. Также важно отметить, что данный обработчик может глушить ошибки от отклоненныхpromise
при срабатывании событияunhandledrejection
, достаточно написать в его функции-обработчикеevent.preventDefault()
Пример 2:
let p = Promise.reject("oops"); p.then(undefined, () => console.log("handled"));Данный пример интересен тем что он вообще не вызывает никаких событий (отсылка к HostPromiseRejectionTracker в объяснении к пятому шагу). Он дважды вызывает HostPromiseRejectionTracker, первый раз с
"reject"
, второй раз с"handle"
. Часть связанная с"reject"
относится к созданию отклоненногоpromise
. А та часть где я используюthen
относится к"handle"
. Когда такое происходит на одной и той же итерации event-loop, не происходит никаких событий связанных с отклонениемpromise
или его обработкой, т.к один из шагов в"handle"
проверяет about-to-be-notified rejected promise list и если там есть нашpromise
, то он его оттуда удаляет и прекращает дальнейшее выполнение алгоритма. В итоге у насpromise
нигде не отслеживается и поставленных с ним задач тоже нет и как следствие нет событий.Внимание: вы можете подумать, что используя
then
без аргументов для отклоненногоpromise
- произойдет обработка ошибки и как следствие отсутствие ошибки. Но увы ошибка возникнет. Это связанно с пробросом ошибки во внутренние интерфейсы языка.Ранее я показывал в главе 3 примеры, связанные с пустыми обработчиками (объяснение abstract closure, который принадлежит NewPromiseReactionJob). Это тот случай когда отсутствуют обработчики и соответственно вместо них происходит либо normal completion либо throw completion. По сути начальный
promise
был заглушен и ошибки не возникло, но из-за того что PerformPromiseThen дляthen
конструкций использует четвертый аргумент, используется запись возможности дляpromise
, которая впоследствии получит из начальногоpromise
значение в новосозданный с помощьюthen
.Таким образом пытаясь заглушить первый
promise
, вы создаете еще один, который получает ошибку из первого и выдает вам его в консоль. Во избежание такого поведения стоит передать пустую функцию, которая пробросит значение через функцию-обработчик, а не через неуправляемый программистом интерфейс.
Пример 3:
let p = Promise.reject("oops"); setTimeout(() => p.then(undefined, () => console.log("handled")), 1000)Этот пример самый последний и самый комплексный, давайте попробуем одолеть его.
Сперва запускается операция HostPromiseRejectionTracker, которая добавит
promise
в about-to-be-notified rejected promise list. Затем на этапе микрозадач произойдет операция notify about rejected promises, которая скопирует about-to-be-notified rejected promises list и проверит его на пустоту, чтобы предотвратить дальнейшие шаги если пусто, также произойдет очистка оригинального about-to-be-notified rejected promises list и в конце будет запланирована задача, в которой находится событиеunhandledrejection
, также эта задача добавитpromise
в outstanding rejected promises weak set если этотpromise
имеет значение в поле[[PromiseIsHandled]]
равноеfalse
. Теперь дожидаемся выполнения этой задачи (без ее шагов выполнения невозможно получить событиеrejectionhandled
).Как только запланированная задача с событием
unhandledrejection
выполнилась - дожидаемся выполнение задачи от setTimeout. Выполнение кода из обработчика setTimeout запустит часть обработки необработанных отклонений - HostPromiseRejectionTracker с аргументом"handle"
. В рамках выполнения данной операции сначала произойдет проверка на наличие нашегоpromise
в about-to-be-notified rejected promises set (в нашем случае его там нет), если его там нет этот шаг дает добро на продолжение выполнение операции. Затем происходит проверкаpromise
в outstanding rejected promises weak set (в нашем случае мы имеем там нашpromise
), если там есть проверяемыйpromise
, значит операция дает добро на продолжение выполнения. После происходит удаление нашегоpromise
из outstanding rejected promises weak set. И в завершении происходит планирование задачи, в которой лежит выстрел событиемrejectionhandled
.Внимание: если текущему примеру изменить setTimeout задержку на
0
, то вы заметите поведение как во втором примере. Результатом такого поведения является то, что задача, которая планируется в notify about rejected promises имеет приоритет меньше чем setTimeout, поэтому если вы хотите получать события вы должны изменить порядок выполнения макрозадач, а то есть добавить задержку setTimeout, как правило хватает даже1мс
.
Ну и как итог всех моих слов - схема (пояснения сделал на английском по привычке, но у вас есть подробнейшее изложение схемы здесь, думаю это не составит труда сопоставить факты):
Заключение: Данные события предназначены для отслеживания отклоненных promise
. Они помогают понять какие promise
были обработаны, а какие остаются существовать необработанными. Таким образом вы можете создать логику, которая будет с чем-то взаимодействовать в случае отклоненных promise
. В крайнем случае это может быть весьма полезно, хотя бы просто для того чтобы заглушить ошибки от отклоненных необработанных promise
. Поэтому важно понимать как они работают, в противном случае это место может быть причиной непонятных ошибок.
Чтобы закрыть тему Promise почти на все 99%, стоит также рассказать о Promise.resolve и Promise.reject. Итак сразу к делу!
Алгоритм: Promise.resolve ( x )
1. Let C be the this value.
2. If C is not an Object, throw a TypeError exception.
3. Return ? PromiseResolve(C, x).
Объяснять тут нечего, за тем исключением что нам нужно раскрыть операцию PromiseResolve (ранее мы ее нигде не рассматривали). Итак в PromiseResolve передается текущий this
и аргумент x
:
Алгоритм: PromiseResolve ( C, x )
1. If IsPromise(x) is true, then
a. Let xConstructor be ? Get(x, "constructor").
b. If SameValue(xConstructor, C) is true, return x.
2. Let promiseCapability be ? NewPromiseCapability(C).
3. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »).
4. Return promiseCapability.[[Promise]].
Объяснение:
- Первый шаг, проверка
x
на то что этоpromise
(по сути смотрится поле[[PromiseState]]
, если оно есть то проверку этот шаг проходит)
- В подшаге
a
получаем уx
значение свойства"constructor"
и записываем вxConstructor
- В подшаге
b
сравниваемxConstructor
и аргумент переданный в эту операциюC
, если они эквивалентны то возвращаем значение аргументаx
- Второй шаг, это создание записи возможности
promise
передавая в качестве аргументаС
, результат сохраняется вpromiseCapability
- Третий шаг, берем у
promiseCapability
его внутреннее поле[[Resolve]]
и вызываем его передавая ему в качестве аргумента значение отx
- Четвертый шаг, возврат поля
[[Promise]]
отpromiseCapability
Как видите объяснение очень простое, результат этой операции возвращается в Promise.resolve.
Обратите внимание на шаг
1.b
когда возвращаетсяx
, это тот случай когда вы в Promise.resolve передаете другойpromise
(это работает только в том случае если конструкторы у сравниваемых экземпляров одинаковые), таким образом вы не получаете какой-то особой обработки, а алгоритм просто обратно вам его отдает:let p = new Promise((res) => res()); Promise.resolve(p) === p; /// trueВ остальных случаях для передаваемого значения в Promise.resolve вы получите соответствующую обработку.
Реализация Promise.resolve:
Операция
NewPromiseCapability
находится здесьPromise._resolve = function(x){ const C = this; if(typeof C !== 'object' && typeof C !== 'function') throw TypeError('this is not object'); const resolvePromise = (C, x) => { if(x instanceof Promise){ const xConstructor = x.constructor; if(C === xConstructor) return x; } const promiseCapability = NewPromiseCapability(C); promiseCapability['[[Resolve]]'](x); return promiseCapability['[[Promise]]']; } return resolvePromise(C, x) }
Теперь давайте рассмотрим антипод - Promise.reject
Алгоритм: Promise.reject ( r )
1. Let C be the this value.
2. Let promiseCapability be ? NewPromiseCapability(C).
3. Perform ? Call(promiseCapability.[[Reject]], undefined, « r »).
4. Return promiseCapability.[[Promise]].
Объяснение:
- Шаг первый, сохранение контекста вызова в
C
- Шаг второй, создание записи возможности promise с передачей аргумента
C
и сохранение результата вpromiseCapability
- Шаг три, вызов значения
[[Reject]]
отpromiseCapability
в качестве функции с передачей аргументаr
- Шаг четыре, возврат значения
[[Promise]]
отpromiseCapability
Этот алгоритм гораздо проще предыдущего, как вы видите.
Реализация Promise.reject:
Операция
NewPromiseCapability
находится здесьPromise._reject = function(r){ const C = this; const promiseCapability = NewPromiseCapability(C); promiseCapability['[[Reject]]'](r); return promiseCapability['[[Promise]]']; }
Заключение: Эти функции являются лишь удобными сокращениями, вместо того, чтобы писать полностью запись где создается экземпляр Promise вручную.
Подумал что некоторые из, читающих данный материал, заинтересуется методом all
конструктора Promise. И поэтому я решил реализовать на js
в соответствии с текстом спецификации ECMAScript
метод Promise.all
Пояснения не прилагаются, вы можете сами поиграться с этими исходниками, работать они должны в большинстве случаев как оригинальный Promise.all.
При запуске обратите внимание, что название метода имеет одно нижнее подчеркивание: Promise._all
. Это сделано для того, чтобы не перезаписывать оригинальный Promise.all.
Надеюсь, вы, при просмотре исходника поймете, почему я не стал объяснять как работает данный метод :)
Реализация Promise.all
(расширенный вариант):
Promise._all = function (iterable) {
let C = this;
let promiseCapability = NewPromiseCapability(C);
let promiseResolve = GetPromiseResolve(C);
if (promiseResolve['[[Type]]'] !== 'normal') {
promiseCapability['[[Reject]]'].call(undefined, promiseResolve['[[Value]]']);
return promiseCapability['[[Promise]]'];
}
promiseResolve = promiseResolve['[[Value]]'];
let iteratorRecord = GetIterator(iterable);
if (iteratorRecord['[[Type]]'] !== 'normal') {
promiseCapability['[[Reject]]'].call(undefined, iteratorRecord['[[Value]]']);
return promiseCapability['[[Promise]]'];
}
iteratorRecord = iteratorRecord['[[Value]]'];
let result = PerformPromiseAll(iteratorRecord, C, promiseCapability, promiseResolve);
if (result['[[Type]]'] !== 'normal') {
if (iteratorRecord['[[Done]]'] === false) {
result = IteratorClose(iteratorRecord, result);
}
if (result['[[Type]]'] !== 'normal') {
promiseCapability['[[Reject]]'].call(undefined, result['[[Value]]']);
return promiseCapability['[[Promise]]'];
}
}
return result['[[Value]]'];
};
function GetPromiseResolve(promiseConstructor) {
try {
let promiseResolve = promiseConstructor.resolve;
if (typeof promiseResolve !== 'function') {
throw TypeError('resolve is not a function');
}
return Completion(promiseResolve);
} catch (e) {
return Completion('throw', e);
}
}
function PerformPromiseAll(iteratorRecord, constructor, resultCapability, promiseResolve) {
try {
let values = [];
let remainingElementsCount = { '[[Value]]': 1 };
let index = 0;
while (true) {
let next = IteratorStep(iteratorRecord);
if (next['[[Type]]'] !== 'normal') {
iteratorRecord['[[Done]]'] = true;
return Completion('throw', next['[[Value]]']);
}
next = next['[[Value]]'];
if (next === false) {
iteratorRecord['[[Done]]'] = true;
remainingElementsCount['[[Value]]'] = remainingElementsCount['[[Value]]'] - 1;
if (remainingElementsCount['[[Value]]'] === 0) {
resultCapability['[[Resolve]]'].call(undefined, values);
}
return Completion(resultCapability['[[Promise]]']);
}
let nextValue = IteratorValue(next);
if (nextValue['[[Type]]'] !== 'normal') {
iteratorRecord['[[Done]]'] = true;
return Completion('throw', nextValue['[[Value]]']);
}
nextValue = nextValue['[[Value]]'];
values.push(undefined);
let nextPromise = promiseResolve.call(constructor, nextValue);
let onFulfilled = function resolver(x) {
if (resolver['[[AlreadyCalled]]']) return;
resolver['[[AlreadyCalled]]'] = true;
let index = resolver['[[Index]]'];
let values = resolver['[[Values]]'];
let promiseCapability = resolver['[[Capability]]'];
let remainingElementsCount = resolver['[[RemainingElements]]'];
values[index] = x;
remainingElementsCount['[[Value]]'] = remainingElementsCount['[[Value]]'] - 1;
if (remainingElementsCount['[[Value]]'] === 0) {
return promiseCapability['[[Resolve]]'].call(undefined, values);
}
return;
};
onFulfilled['[[AlreadyCalled]]'] = false;
onFulfilled['[[Index]]'] = index;
onFulfilled['[[Values]]'] = values;
onFulfilled['[[Capability]]'] = resultCapability;
onFulfilled['[[RemainingElements]]'] = remainingElementsCount;
remainingElementsCount['[[Value]]'] = remainingElementsCount['[[Value]]'] + 1;
nextPromise.then(onFulfilled, resultCapability['[[Reject]]']);
index = index + 1;
}
} catch (e) {
return Completion('throw', e);
}
}
function NewPromiseCapability(C) {
if (!(C.prototype && C.prototype.constructor === C)) throw TypeError('C is not a constructor');
const record = {
'[[Promise]]': undefined,
'[[Resolve]]': undefined,
'[[Reject]]': undefined,
};
const closure = function (resolve, reject) {
if (record['[[Resolve]]']) throw TypeError('Resolve function is not undefined');
if (record['[[Reject]]']) throw TypeError('Reject function is not undefined');
record['[[Resolve]]'] = resolve;
record['[[Reject]]'] = reject;
};
const promise = Reflect.construct(C, [closure]);
if (typeof record['[[Resolve]]'] !== 'function') throw TypeError('Resolve function is not callable');
if (typeof record['[[Reject]]'] !== 'function') throw TypeError('Reject function is not callable');
record['[[Promise]]'] = promise;
return record;
}
function GetIterator(obj, hint, method) {
try {
if (hint == null) hint = 'sync';
if (method == null) {
if (hint === 'async') {
method = obj[Symbol.asyncIterator];
if (method === 'undefined') {
/// По идее должны быть следующие строки:
///
/// let syncMethod = obj[Symbol.iterator];
/// let syncIteratorRecord = GetIterator(obj, 'sync', syncMethod);
/// return CreateAsyncFromSyncIterator(syncIteratorRecord);
///
/// Но я их убрал и поставил ошибку, так как реализация преобразователя
/// Слишком сложная и в понимании Promise.all не нужна
///
/// Кому интересно почему я не стал реализовывать вот ссылка на спецификацию
/// https://tc39.es/ecma262/#sec-createasyncfromsynciterator
///
/// P.S Когда приступлю писать про итераторы, то придется уже тогда описать полностью, но не сейчас
throw Error('There is no thunk from sync iterator to async iterator');
}
} else {
method = obj[Symbol.iterator];
}
let iterator = method?.call(obj);
if ((typeof iterator !== 'object' && typeof iterator !== 'function') || iterator === null) {
throw TypeError("Iterator can't be non-object");
}
let nextMethod = iterator.next;
let iteratorRecord = {
'[[Iterator]]': iterator,
'[[NextMethod]]': nextMethod,
'[[Done]]': false,
__proto__: {
[Symbol.toStringTag]: 'Iterator Record',
},
};
return Completion(iteratorRecord);
}
} catch (e) {
return Completion('throw', e);
}
}
function IteratorNext(iteratorRecord, value) {
try {
let result;
if (value == null) {
result = iteratorRecord['[[NextMethod]]'].call(iteratorRecord['[[Iterator]]']);
} else {
result = iteratorRecord['[[NextMethod]]'].call(iteratorRecord['[[Iterator]]'], value);
}
if ((typeof result !== 'object' && typeof result !== 'function') || result == null) {
throw TypeError('next() should return object');
}
return Completion(result);
} catch (e) {
return Completion('throw', e);
}
}
function IteratorComplete(iterResult) {
try {
return Completion(Boolean(iterResult.done));
} catch (e) {
return Completion('throw', e);
}
}
function IteratorValue(iterResult) {
try {
return Completion(iterResult.value);
} catch (e) {
return Completion('throw', e);
}
}
function IteratorStep(iteratorRecord) {
let result = IteratorNext(iteratorRecord);
if (result['[[Type]]'] !== 'normal') {
return result;
}
result = result['[[Value]]'];
let done = IteratorComplete(result);
if (done['[[Type]]'] !== 'normal') {
return done;
}
done = done['[[Value]]'];
return Completion(done === true ? false : result);
}
function IteratorClose(iteratorRecord, completion) {
try {
let GetMethod = (V, P) => {
try {
let func = V[P];
if (func == null) {
return Completion(undefined);
}
if (typeof func !== 'function') {
throw TypeError("It's not a function");
}
return Completion(func);
} catch (e) {
return Completion('throw', e);
}
};
let iterator = iteratorRecord['[[Iterator]]'];
let innerResult = GetMethod(iterator, 'return');
if (innerResult['[[Type]]'] === 'normal') {
let _return = innerResult['[[Value]]'];
if (_return === undefined) {
return completion;
}
innerResult = _return.call(iterator);
}
if (completion['[[Type]]'] === 'throw') return completion;
if (innerResult['[[Type]]'] === 'throw') return innerResult;
if ((typeof innerResult['[[Value]]'] !== 'object' && typeof innerResult['[[Value]]'] !== 'function') || innerResult['[[Value]]'] === null) {
throw TypeError('closing iterator should return value of object type');
}
return completion;
} catch (e) {
return Completion('throw', e);
}
}
function Completion(type, value, target) {
if (arguments.length === 1) {
value = type;
type = undefined;
}
return {
'[[Type]]': type ?? 'normal',
'[[Value]]': value,
'[[Target]]': target ?? 'empty',
__proto__: {
[Symbol.toStringTag]: 'Completion',
},
};
}
Более короткая версия Promise.all
(многие концепции ECMAScript
опущены):
function PromiseAll(iterable, C) {
try {
C = C ?? Promise;
let NewPromiseCapability = function (C) {
if (!(C.prototype && C.prototype.constructor === C)) throw TypeError('C is not a constructor');
const record = {
'[[Promise]]': undefined,
'[[Resolve]]': undefined,
'[[Reject]]': undefined,
};
const closure = function (resolve, reject) {
if (record['[[Resolve]]']) throw TypeError('Resolve function is not undefined');
if (record['[[Reject]]']) throw TypeError('Reject function is not undefined');
record['[[Resolve]]'] = resolve;
record['[[Reject]]'] = reject;
};
const promise = Reflect.construct(C, [closure]);
if (typeof record['[[Resolve]]'] !== 'function') throw TypeError('Resolve function is not callable');
if (typeof record['[[Reject]]'] !== 'function') throw TypeError('Reject function is not callable');
record['[[Promise]]'] = promise;
return record;
};
let promiseCapability = NewPromiseCapability(C);
let promiseResolve = C.resolve;
if (typeof promiseResolve !== 'function') throw Error('resolve is not a function');
let iterator = iterable[Symbol.iterator || Symbol.asyncIterator].call(iterable);
let iteratorRecord = {
'[[Iterator]]': iterator,
'[[NextMethod]]': iterator.next,
'[[Done]]': false,
__proto__: {
[Symbol.toStringTag]: 'Iterator Record',
},
};
try {
let result = PerformPromiseAll(iteratorRecord, C, promiseCapability, promiseResolve);
return result;
} catch (e) {
if (iteratorRecord['[[Done]]'] === false) {
let it = iteratorRecord['[[Iterator]]'];
if (it.return) {
e = it.return();
}
}
promiseCapability['[[Reject]]'].call(undefined, e);
return promiseCapability['[[Promise]]'];
}
} catch (e) {
throw e;
}
}
function PerformPromiseAll(iteratorRecord, constructor, resultCapability, promiseResolve) {
try {
let values = [];
let remainingElementsCount = { '[[Value]]': 1 };
let index = 0;
while (true) {
let next;
try {
next = iteratorRecord['[[NextMethod]]'].call(iteratorRecord['[[Iterator]]']);
} catch (e) {
iteratorRecord['[[Done]]'] = true;
throw e;
}
if (next.done === true) {
iteratorRecord['[[Done]]'] = true;
remainingElementsCount['[[Value]]'] = remainingElementsCount['[[Value]]'] - 1;
if (remainingElementsCount['[[Value]]'] === 0) {
resultCapability['[[Resolve]]'](values);
}
return resultCapability['[[Promise]]'];
}
let nextValue;
try {
nextValue = next.value;
} catch (e) {
iteratorRecord['[[Done]]'] = true;
throw e;
}
values.push(undefined);
let nextPromise = promiseResolve.call(constructor, nextValue);
let onFulfilled = function resolver(x) {
if (resolver['[[AlreadyCalled]]']) return;
resolver['[[AlreadyCalled]]'] = true;
let index = resolver['[[Index]]'];
let values = resolver['[[Values]]'];
let promiseCapability = resolver['[[Capability]]'];
let remainingElementsCount = resolver['[[RemainingElements]]'];
values[index] = x;
remainingElementsCount['[[Value]]'] = remainingElementsCount['[[Value]]'] - 1;
if (remainingElementsCount['[[Value]]'] === 0) {
return promiseCapability['[[Resolve]]'].call(undefined, values);
}
return;
};
onFulfilled['[[AlreadyCalled]]'] = false;
onFulfilled['[[Index]]'] = index;
onFulfilled['[[Values]]'] = values;
onFulfilled['[[Capability]]'] = resultCapability;
onFulfilled['[[RemainingElements]]'] = remainingElementsCount;
remainingElementsCount['[[Value]]'] = remainingElementsCount['[[Value]]'] + 1;
nextPromise.then(onFulfilled, resultCapability['[[Reject]]']);
index = index + 1;
}
} catch (e) {
throw e;
}
}
Можно было бы и другие подобные методы расписать, но они монструозны подобно Promise.all и достаточно понять концепцию того как происходит потоковая обработка объектов, чтобы писать подобные методы самостоятельно.
Заключение: Понимание того как работают подобные методы, дает вам очень мощное знание как работать с наборами promise
.
Promise - это не просто объект, который описывает спецификация ECMAScript
, это объект, который несет ключевое бремя в современном программировании в сети интернет, он пришел как замена функциям обратного вызова, чтобы внести ясность и простоту в асинхронный код. Его реализация, как вы могли заметить, является достаточно нетривиальной задачей, но его возможности этого стоят и знание того как работают promises
- ключ к правильному написанию кода в проектах любой сложности.
My intentions will never end.
- https://tc39.es/ecma262/#sec-promise-executor
- https://tc39.es/ecma262/#sec-properties-of-promise-instances
- https://tc39.es/ecma262/#sec-createresolvingfunctions
- https://tc39.es/ecma262/#sec-promisecapability-records
- https://tc39.es/ecma262/#sec-promise-resolve-functions
- https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob
- https://tc39.es/ecma262/#sec-promise-reject-functions
- https://tc39.es/ecma262/#sec-fulfillpromise
- https://tc39.es/ecma262/#sec-rejectpromise
- https://tc39.es/ecma262/#sec-triggerpromisereactions
- https://tc39.es/ecma262/#sec-promise.resolve
- https://tc39.es/ecma262/#sec-promise-resolve
- https://tc39.es/ecma262/#sec-promise.reject
- https://tc39.es/ecma262/#sec-promise.prototype.then
- https://tc39.es/ecma262/#sec-promise.prototype.catch
- https://tc39.es/ecma262/#sec-promise.prototype.finally
- https://tc39.es/ecma262/#sec-performpromisethen
- https://tc39.es/ecma262/#sec-promisereaction-records
- https://tc39.es/ecma262/#sec-newpromisereactionjob
- https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob
- https://tc39.es/ecma262/#sec-newpromisecapability
- https://tc39.es/ecma262/#sec-promise.all
- https://tc39.es/ecma262/#sec-execution-contexts
- https://tc39.es/ecma262/#execution-context-stack
- https://tc39.es/ecma262/#sec-code-realms
- https://tc39.es/ecma262/#sec-agents
- https://tc39.es/ecma262/#sec-abstract-closure
- https://tc39.es/ecma262/#sec-jobcallback-records
- https://tc39.es/ecma262/#sec-createbuiltinfunction
- https://tc39.es/ecma262/#sec-get-o-p
- https://tc39.es/ecma262/#sec-construct
- https://tc39.es/ecma262/#sec-call
- https://tc39.es/ecma262/#sec-ecmascript-language-types
- https://tc39.es/ecma262/#sec-list-and-record-specification-type
- https://tc39.es/ecma262/#function-object
- https://tc39.es/ecma262/#sec-completion-ao
- https://tc39.es/ecma262/#sec-normalcompletion
- https://tc39.es/ecma262/#sec-throwcompletion
- https://tc39.es/ecma262/#sec-completion-record-specification-type
- https://tc39.es/ecma262/#sec-hostmakejobcallback
- https://tc39.es/ecma262/#sec-hostcalljobcallback
- https://tc39.es/ecma262/#sec-hostenqueuepromisejob
- https://tc39.es/ecma262/#sec-host-promise-rejection-tracker
- https://tc39.es/ecma262/#current-realm
- https://tc39.es/ecma262/#sec-script-records
- https://tc39.es/ecma262/#sec-abstract-module-records
- https://tc39.es/ecma262/#sec-import-call-runtime-semantics-evaluation
- https://tc39.es/ecma262/#sec-parse-script
- https://html.spec.whatwg.org/multipage/webappapis.html#creating-scripts
- https://html.spec.whatwg.org/multipage/webappapis.html#script-structs
- https://html.spec.whatwg.org/multipage/scripting.html#the-script-element
- https://html.spec.whatwg.org/multipage/webappapis.html#hostmakejobcallback
- https://html.spec.whatwg.org/multipage/webappapis.html#hostcalljobcallback
- https://html.spec.whatwg.org/multipage/webappapis.html#hostenqueuepromisejob
- https://html.spec.whatwg.org/multipage/webappapis.html#the-hostpromiserejectiontracker-implementation
- https://html.spec.whatwg.org/multipage/webappapis.html
- https://html.spec.whatwg.org/multipage/window-object.html#the-window-object
- https://html.spec.whatwg.org/multipage/workers.html#the-workerglobalscope-common-interface
- https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
- https://html.spec.whatwg.org/multipage/window-object.html#set-up-a-window-environment-settings-object
- https://html.spec.whatwg.org/multipage/webappapis.html#concept-realm-settings-object
- https://html.spec.whatwg.org/multipage/webappapis.html#incumbent-settings-object
- https://html.spec.whatwg.org/multipage/webappapis.html#backup-incumbent-settings-object-stack
- https://html.spec.whatwg.org/multipage/webappapis.html#concept-incumbent-realm
- https://html.spec.whatwg.org/multipage/webappapis.html#concept-relevant-realm
- https://html.spec.whatwg.org/multipage/webappapis.html#active-script
- https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
- https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-microtask
- https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout
- https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-setinterval
- https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#microtask-queuing
- https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#dom-animationframeprovider-requestanimationframe
- https://html.spec.whatwg.org/multipage/browsing-the-web.html#read-html
- https://html.spec.whatwg.org/multipage/webappapis.html#generic-task-sources
- https://html.spec.whatwg.org/multipage/webappapis.html#user-interaction-task-source
- https://html.spec.whatwg.org/multipage/webappapis.html#task-queue
- https://html.spec.whatwg.org/multipage/webappapis.html#check-if-we-can-run-script
- https://html.spec.whatwg.org/multipage/webappapis.html#prepare-to-run-script
- https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script
- https://html.spec.whatwg.org/multipage/webappapis.html#prepare-to-run-a-callback
- https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-a-callback
- https://html.spec.whatwg.org/multipage/webappapis.html#report-the-exception
- https://html.spec.whatwg.org/multipage/web-messaging.html#posting-messages
- https://html.spec.whatwg.org/multipage/history.html#location-object-navigate
- https://html.spec.whatwg.org/multipage/webappapis.html#concept-script-base-url
- https://html.spec.whatwg.org/multipage/urls-and-fetching.html#document-base-url
- https://html.spec.whatwg.org/multipage/webappapis.html#running-script
- https://html.spec.whatwg.org/multipage/webappapis.html#classic-script
- https://html.spec.whatwg.org/multipage/webappapis.html#muted-errors
- https://html.spec.whatwg.org/multipage/webappapis.html#settings-object
- https://html.spec.whatwg.org/multipage/webappapis.html#about-to-be-notified-rejected-promises-list
- https://html.spec.whatwg.org/multipage/webappapis.html#outstanding-rejected-promises-weak-set
- https://html.spec.whatwg.org/multipage/webappapis.html#concept-settings-object-global
- https://html.spec.whatwg.org/multipage/webappapis.html#queue-a-global-task
- https://html.spec.whatwg.org/multipage/webappapis.html#dom-manipulation-task-source
- https://html.spec.whatwg.org/#document
- https://html.spec.whatwg.org/#browsing-context
- https://html.spec.whatwg.org/multipage/webappapis.html#notify-about-rejected-promises
- https://dom.spec.whatwg.org/#concept-event-fire
- https://w3c.github.io/longtasks/
- https://w3c.github.io/requestidlecallback/#dom-window-requestidlecallback
Отлично!