Skip to content

Instantly share code, notes, and snippets.

@dSalieri
Last active March 14, 2024 20:11
Show Gist options
  • Star 28 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dSalieri/9bbfad303842f9e4800337894e5cbc1c to your computer and use it in GitHub Desktop.
Save dSalieri/9bbfad303842f9e4800337894e5cbc1c to your computer and use it in GitHub Desktop.
Promise изнутри

Автор: dSalieri

Версия ECMAScript, используемая в объяснении: Draft ECMA-262 / June 24, 2022

Версия WHATWG, используемая в объяснении: Living Standard - 22 August 2022

Последнее изменение документа: 13.12.2022


Оглавление:

Вместо вступления

That was and will continue seriously 48696c5g665g725c as you see.

Siu Pai Mei

 Имейте честь - стремитесь к правде,
 А ложь оставьте слабакам,
 Для них, забвенье лучше правды,
 Для нас, ложь - корень зла!

 dSalieri(c)


Promise - асинхронная синхронность.

Часть 1: Оболочка promise, составляющие и механизм разрешения

Что такое 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, так как именно эти алгоритмы являются потребителями его сущностей.

Часть 2: Методы promise, взаимодействие механизмов 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 ( 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. Дальше вы познакомитесь с этапами создания задачи, планирования и выполнения.

Часть 3: Создание задачи, ее планирование и выполнение

Примечание: В этой главе я расскажу о том как создается задача, как она планируется, также укажу когда она будет исполнена. Мне придется залезть в смежную спецификацию whatwg, так как именно она демонстрирует алгоритмы Host-процедур (ECMAScript этого не делает). Ко всему прочему я затрону всем небезызвестный event-loop (цикл событий), на котором продемонстрирую в какой момент времени будет задействована задача (микрозадача - термин whatwg)

Эта глава полностью совместима с другими концепциями такими как async или async generators, если вы хотели узнать как они это делают, то вы по адресу.

Перед началом нужно прояснить пару деталей...

Данная часть про расширение концепции Realm спецификацией whatwg.

Есть несколько способов определения нужного для наших задач Realm в тот или иной момент времени, существует несколько концепций определенных whatwg (не все из них используются для promise, это больше для полной картины определения Realm):

  • Entry

    Это соответствует скрипту, который является инициатором для других скриптов или функций. Эта концепция часто используется web-разработчиками когда говорят про так называемый "entry-point" или "точка входа" (по-человечески).

  • Incumbent

    Это соответствует самой последней введенной функции или скрипту в стеке или это функция или скрипт, которые изначально запланировали текущий обратный вызов; отлично описывает эту концепцию алгоритм incumbent settings object.

  • Current

    Это соответствует текущему вызову функции. Как правило спецификация ECMAScript оперирует именно этой концепцией у себя.

  • Relevant

    Каждый объект платформы имеет relevant Realm. При написании алгоритмов часто-используемый объект платформы - это значение ключевого слова this. И в зависимости от того как будет вызываться алгоритм будет определяться его relevant Realm (под объектами платформы подразумеваются объекты, которые созданы данной платформой; объекты, которые описаны в ECMAScript сюда не входят).

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

На этом все. Вот ссылка на страницу, где вы можете отыскать все эти термины, что я использовал выше (мои пояснения немного отличаются от спецификации). Если есть желание можете подробно изучить все эти интерфейсы самостоятельно, там же есть ссылки на гитхаб об уменьшении влияния концепций Entry и Incumbent.

Итак в спецификации ECMAScript есть три места где создается задача и ставится в очередь:

В каждом из случаев происходят следующие операции, которые участвуют в создании задачи, постановки ее в очередь планировщика и выполнения:

  • 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, тогда

    • Пусть 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, тогда

    • Если result это abrupt completion, тогда

Как вы поняли этот алгоритм конкретно занимается постановкой микрозадач в очередь микрозадач. Шаги микрозадачи начнут выполнятся только тогда когда наступит время этой микрозадачи, на данном этапе определен алгоритм микрозадач.

После того как мы поставили микрозадачу в очередь микрозадач, осталось ее дождаться, вспомните ликбез по 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, но это тема отдельного разговора)

На этом этапе завершается выполнение поставленной микрозадачи, если существуют еще микрозадачи то самая первая в очереди будет вытолкнута из стека микрозадач, а затем исполнена и так пока стек микрозадач не опустеет, после чего наступит макрозадача. Также не стоит забывать что микрозадачи могут создать новые микрозадачи и созданные микрозадачи будут помещены в этот же стек и как следствие из этого выполнение стека микрозадач до полного опустошения.

Ну и чтобы все подытожить - вот схема всех этих действий:

Initial call - начальный вызов, то что начинает процесс связанный с promise

Awaiting microtask checkpoint - это этап ожидания выполнения микрозадач

microtask model(enqueueing and performing)

Заключение: Эта глава показала вам когда и где начинается планирование микрозадачи, куда она попадает после планирования и что происходит когда наступает ее время исполнения. Бонусом вы увидели механизм event-loop, который показал на каком этапе происходит выполнение этих микрозадач. На этом все.

Часть 4: События unhandledrejection и rejectionhandled

Ранее я абстрагировался от описания этих событий, так как эти события связаны с 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 и в зависимости от обстоятельств выдавать то или иное событие.

Если сейчас совсем еще непонятно, что делает эта операция и почему здесь только одна задача, которая выстреливает событием - все нормально.

В этом алгоритме да и вообще есть две коллекции, которые отвечают за отслеживание отклоненных 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 events

Заключение: Данные события предназначены для отслеживания отклоненных promise. Они помогают понять какие promise были обработаны, а какие остаются существовать необработанными. Таким образом вы можете создать логику, которая будет с чем-то взаимодействовать в случае отклоненных promise. В крайнем случае это может быть весьма полезно, хотя бы просто для того чтобы заглушить ошибки от отклоненных необработанных promise. Поэтому важно понимать как они работают, в противном случае это место может быть причиной непонятных ошибок.

Часть 5: Конструкторные методы Promise.resolve и Promise.reject

Чтобы закрыть тему 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 вручную.

Часть 6: Демонстрация Promise.all

Подумал что некоторые из, читающих данный материал, заинтересуется методом 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.


Список ссылок данного документа:

@Peter-developer01
Copy link

Отлично!

@Egor-Demeshko
Copy link

спасибо) с первого захода не осилил.
сначала сам спецификацию крутил, потом ваша статья в коммента на learn.javascript. дочитал за двое суток) но в конце уже не смог фокусировать внимание. явно прийдется возращаться.

@dSalieri
Copy link
Author

dSalieri commented Apr 21, 2023

@Egor-Demeshko ну, я и не рассчитывал на то чтобы ее можно было осилить за один заход. Только вдумчивое чтение, вероятно с собственными заметками у себя в тетради, так как держать такой пласт информации в отрыве от опыта чтения самой спецификации - довольно тяжелая задача.

Предполагаю что очень тяжело пошла 3 часть, но тут и понятно ECMAScript + WHATWG.

В любом случае пишите вопросы - посмотрим и разберем непонятные моменты.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment