Skip to content

Instantly share code, notes, and snippets.

@dSalieri
Last active July 2, 2024 14:06
Show Gist options
  • Save dSalieri/eb698ab2409c3672b778c33e99455bbd to your computer and use it in GitHub Desktop.
Save dSalieri/eb698ab2409c3672b778c33e99455bbd to your computer and use it in GitHub Desktop.
Аргумент resolve внутри Promise и Promise.resolve работают ли они одинаково?

Вопрос: Есть две цепочки promise, которые выводят сообщения в консоль. Первый выводит: tick1, tick2, tick3. Второй: tick3, tick1, tick2

Код 1:

/// Код 1
const p = new Promise(resovle => setTimeout(resovle));

new Promise(resolve => resolve(p)).then(() => {
    console.log("tick 3");
});

p.then(() => {
    console.log("tick 1");
}).then(() => {
    console.log("tick 2");
});

Код 2:

const p = new Promise(resolve => setTimeout(resolve));

Promise.resolve(p).then(() => {
    console.log("tick 3");
});

p.then(() => {
    console.log("tick 1");
}).then(() => {
    console.log("tick 2");
});

Почему вывод разный, разве конструкции не эквивалентны?


Ответ:

Прежде чем начать я введу обозначения чтобы не запутаться по ходу объяснения.

Ремарка об обозначениях:

Promise - конструктор
promise - экземпляр Promise

Итак, при создании promise, происходят следующие вещи:

1) Note: Поля в квадратных скобках - внутренние поля, которые используются механизмами внутри реализации самого языка и недоступны извне. Также эти поля принадлежат конкретному promise: [[PromiseState]], [[PromiseResult]], [[PromiseFulfillReactions]], [[PromiseRejectReactions]], [[PromiseIsHandled]]
2) [[PromiseState]] устанавливается в pending
3) [[PromiseFulfillReactions]] устанавливается в пустой список микрозадач fulfill
4) [[PromiseRejectReactions]] устанавливается в пустой список микрозадач reject
5) [[PromiseIsHandled]] устанавливается в false (поле о том было ли Promise обработано)
6) Создаются разрешающие функции: resolve и reject через функцию CreateResolvingFunctions
7) Вызывается функция-executor, это та функция, которая передается в Promise при создании экземпляра, в которую передаются resolve и reject, которые создали выше
8) Если вдруг происходит ошибка с внезапным завершением, то происходит вызов reject функции
9) Возвращаем наш созданный promise
10) Note: [[PromiseResult]] - используется в других операциях, здесь он просто определяется, имеет значение undefined

Так теперь вы знаете об алгоритме new Promise(executorFunction)

Начинаем погружение...

Прежде чем мы перейдем к Promise.resolve, нужно выяснить, что делает классический resolve в executor.

Итак я упоминал алгоритм, который создает разрешающие функции resolve и reject. Это CreateResolvingFunctions, давайте зайдем в него. Тут происходит достаточное количество шагов инициализации и создания этих функций, но мы рассмотрим только важную часть. Так как нас интересует часть с resolve, то шаг 2 - то что нам надо, который говорит, что шаги которые описаны в Promise Resolve Functions будут присвоены в специальную переменную, от себя скажу, что эти шаги будут запущены в тот момент когда resolve непосредственно будет вызван в коде. То есть функция resolve, которая будет исполняться при вызове это и есть Promise Resolve Functions. Далее начинается самое интересное, потому что в resolve может быть передано что угодно; опишу базовые кейсы:

  1. resolve("hi") - передается примитив, таким образом это соответствует шагу 8 этого алгоритма, далее такой resolve будет прямо здесь же разрешен и promise получит состояние fulfilled со значением "hi". Все это произойдет до того как синхронный код будет прочитан полностью.
  2. resolve(new Promise((resolve) => resolve("hi"))) - вот тут ситуация сложнее, так как передаем сложный объект и его нужно разрешить (ваш случай как раз такой). За разрешение таких записей отвечают строки 13, 14, 15, 16. Thenable объекты рассматривать не будем (хотя по большей части наш пример и есть thenable объект, только официальный). Ну-с при таком раскладе когда в resolve лежит объект promise, наш promise, который мы пытаемся разрешить, не разрешается на месте и он будет разрешен в соответствии с очередью микрозадач.

На данном этапе что мы можем понять? Примитивные значения разрешаются сразу, до окончания синхронного кода, а promise (thenable-объекты) разрешаются после и в соответствии с очередью микрозадач UA.

Так надеюсь с resolve внутри promise все ясно. Теперь время того чтобы понять как работает Promise.resolve и почему результат совершенно другой.

Открываем спецификацию с Promise.resolve и видим, что вызывается PromiseResolve в которую передаются два аргумента первый тот что является this - это Promise в данном случае и второй аргумент - это передаваемое значение. Заходим в PromiseResolve и давайте рассмотрим в таком же стиле как я сделал это с обычным resolve.

  1. Promise.resolve("hi") - передается примитив, и это соответствует шагам 2, 3, 4. Резонный вопрос: соответствуют ли эти шаги классическому resolve? Увы, но не совсем. Классический resolve озадачен лишь тем чтобы разрешить promise, а вот Promise.resolve обязан создать оболочку promise. Только 3ий шаг соответствует классическому resolve.
  2. Promise.resolve(new Promise((resolve) => resolve("hi"))) - передается promise, и это соответствует шагу 1.b. Я долго думал как описать этот шаг, поэтому скажу вкратце, этот шаг проверяет значение свойства "constructor" в передаваемом объекте, и если это свойство равно Promise, тогда мы возвращаем передаваемый promise. Звучит это сложно. Поэтому вот как это выглядит наглядно: Promise.resolve(new Promise((resolve) => resolve("hi"))) получается вот что new Promise((resolve) => resolve("hi")). По сути реализация избавляется от Promise.resolve и отдает бразды управления непосредственно самому promise.

Надеюсь теперь теперь видно различие между такими "схожими" resolve, конструкциями.

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

Примечание к примерам:

Помимо очереди/стека макрозадач и микрозадач UA будут еще очереди promise. Очереди promise в данном случае рассматриваются только для fulfilled promise, и соответственно они соответствуют полю [[PromiseFulfillReactions]]. Rejected promise не рассматриваются, так как их даже нет в нашем коде.

Первый пример:

const p = new Promise(resolve => setTimeout(resolve));
const p2 = new Promise(resolve => resolve(p));

const der_p2 = p2.then(() => {
    console.log("tick 3");
});

const der = p.then(() => {
    console.log("tick 1");
});

const der2 = der.then(() => {
    console.log("tick 2");
});

Все выполняется в порядке очереди

Макро стадия

Так как скрипт является макрозадачей априори, то он и выполняется.
setTimeout - создает и отправляет в планировщик UA макрозадачу, пусть ее имя #_task1
resolve(p) - создает и отправляет в планировщик UA микрозадачу, пусть ее имя #task1
p2.then() - создает и отправляет микрозадачу в очередь ожидания, пусть ее имя #task2
p.then() - создает и отправляет микрозадачу в очередь ожидания, пусть ее имя #task3
der.then() - создает и отправляет микрозадачу в очередь ожидания, пусть ее имя #task4
Очередь p2: [#task2]
Очередь p: [#task3]
Очередь der: [#task4]
Стек микрозадач: [#task1]
Стек макрозадач: [#_task1]

Микро стадия

#task1 - микрозадача выполняется (не разрешает p2, эта задача нужна для того чтобы создать совместимость между этим promise и тем что вкладывается в качестве разрешения), создается и отправляется в очередь p новая микрозадача, назовем ее #task5
Очередь p2: [#task2]
Очередь p: [#task3, #task5]
Очередь der: [#task4]
Стек микрозадач: []
Стек макрозадач: [#_task1]

Макро стадия

resolve в setTimeout является кодом макрозадачи, а поэтому он выполняется, но так как передается пустая функция без аргументов, то ничего в resolve не передается. Данный resolve, запускает процесс разрешения p, и отправляет связанные с ним микрозадачи в планировщик UA
Очередь p2: [#task2]
Очередь p: []
Очередь der: [#task4]
Стек микрозадач: [#task3, #task5]
Стек макрозадач: []

Микро стадия

#task3 - микрозадача выполняется, выводит "tick 1", разрешает der, отправляет в планировщик UA связанные с der задачи
#task5 - микрозадача выполняется, разрешает p2, отправляет в планировщик UA связанные с p2 задачи
Очередь p2: []
Очередь p: []
Очередь der: []
Стек микрозадач: [#task4, #task2]
Стек макрозадач: []

Стек микрозадач еще не пуст продолжаем оставаться в стадии микро

#task4 - микрозадача выполняется, выводит "tick 2", разрешает der2, отправлять в планировщик UA нечего
#task2 - микрозадача выполняется, выводит "tick 3", разрешает der_p2, отправлять в планировщик UA нечего
Стек микрозадач: []
Стек макрозадач: []

Теперь второй пример:

const p = new Promise(resolve => setTimeout(resolve));
const p2 = Promise.resolve(p);

const der_p2 = p2.then(() => {
    console.log("tick 3");
});

const der = p.then(() => {
    console.log("tick 1");
});

const der2 = der.then(() => {
    console.log("tick 2");
});

Все выполняется в порядке очереди

Макро стадия

Так как скрипт является макрозадачей априори, то он и выполняется.
setTimeout - создает и отправляет в планировщик UA макрозадачу, пусть ее имя #_task1
Promise.resolve() - возвращает переданный в него аргумент это promise p, теперь p2 === p, проверить это можно вставив код с консолью после строчки с константой p2
p2.then() - создает и отправляет микрозадачу в очередь ожидания, пусть ее имя #task1
p.then() - создает и отправляет микрозадачу в очередь ожидания, пусть ее имя #task2
der.then() - создает и отправляет микрозадачу в очередь ожидания, пусть ее имя #task3
Очередь p === p2: [#task1, #task2]
Очередь der: [#task3]
Стек микрозадач: []
Стек макрозадач: [#_task1]

Микро стадия

Пусто
Очередь p === p2: [#task1, #task2]
Очередь der: [#task3]
Стек микрозадач: []
Стек макрозадач: [#_task1]

Макро стадия

resolve в setTimeout является кодом макрозадачи, а поэтому он выполняется, но так как передается пустая функция без аргументов, то ничего в resolve не передается. Данный resolve, запускает процесс разрешения p или p2, и отправляет связанные с ним микрозадачи в планировщик UA
Очередь p === p2: []
Очередь der: [#task3]
Стек микрозадач: [#task1, #task2]
Стек макрозадач: []

Микро стадия

#task1 - микрозадача выполняется, выводит "tick 3", разрешает der_p2, отправлять в планировщик UA нечего
#task2 - микрозадача выполняется, выводит "tick 1", разрешает der, отправляет в планировщик UA связанные с der задачи
Очередь p === p2: []
Очередь der: []
Стек микрозадач: [#task3]
Стек макрозадач: []

Стек микрозадач еще не пуст продолжаем оставаться в стадии микро

#task3 - микрозадача выполняется, выводит "tick 2", разрешает der2, отправлять в планировщик UA нечего
Стек микрозадач: []
Стек макрозадач: []

Все :)

@dSalieri
Copy link
Author

dSalieri commented Feb 24, 2023

@Welldar а вопрос про "совместимость", так и думал что когда-нибудь кто-то и спросит.

Такая совместимость срабатывает только на promise и объектах имеющих свойство then, в остальных случаях этого не происходит.

Что ж раскрою этот момент детальнее.

Итак чтобы создать тот же случай что вы и спрашиваете возьмем такую запись:

const a = new Promise((res) => setTimeout(() => res(5), 1000));
const b = new Promise((res) => res(a));
console.log(b);

Такая запись console.log(b) не может получить в синхронном коде (или макрозадаче) результат со статусом fulfilled. Для того чтобы вычислить значение и получить разрешенный статус, ей придется выполнить и микрозадачи, а это уже является асинхронным кодом.

Спецификация накладывает на res (он же является [[Resolve]]) одну особенность когда вместо разрешения promise на месте он начинает процесс создания и планирования микрозадачи.

Зачем спросите вы?

Ну предположим вы хотите разрешить один promise через другой (наш пример как раз такой), но проблема в том что вы не знаете когда вложенный promise разрешится, именно поэтому сделана такая особенность дизайна разрешения promise. Грубо говоря он создает then запись для вложенного promise, который при своем выполнении разрешит наш внешний promise. Кстати говоря это нам позволит точно понять порядок запланированных микрозадач.

Если рассматривать с точки зрения логики то вложенное разрешение на then выглядит так.

const a = new Promise((res) => setTimeout(() => res(5), 1000));
const resolve = {"[[Resolve]]": undefined};
const b = new Promise((res) => resolve["[[Resolve]]"] = res);
a.then((value) => resolve["[[Resolve]]"](value));
console.log(b)

Запутанно да? Но именно так выглядит база вложенного разрешения. Все это необходимо чтобы четко детерминировать последовательность разрешений всех promise.

Если вам интересно узнать во всех подробностях как работает promise, приглашаю почитать мою статью о promise, там описан весь механизм от А до Я.

P.S На ваш комментарий наткнулся случайно, так что тегайте имя пользователя если хотите чтобы человек получил оповещение (вам на заметку).

@Welldar
Copy link

Welldar commented Feb 27, 2023

@dSalieri ого, не знал что такие фокусы можно делать, а именно присваивать ф-ию resolve глобальной переменной и вызывать ее потом. Не много не понимаю только, это работает благодаря тому что все функции в JS формируют замыкание по умолчанию? Просто все эти встроенные функции res и rej одинаковые у каждого экземпляра Promise, а значит они отличаются только по контексту создания, и именно благодаря этому работает второй пример?
P.S. Приступаю к вашему разбору Promise.

@dSalieri
Copy link
Author

dSalieri commented Feb 28, 2023

@Welldar да это не фокусы, мы же таким образом можем вытаскивать аргументы переданные в функцию не только в Promise. Это не исключительная особенность. Но так лучше не делать, это нарушает интерфейс Promise и вообще по сути это антипаттерн. Это была лишь укороченная демонстрация как [[Resolve]] разрешает вложенный в него объект promise.

Для каждого экземпляра Promise создаются свои [[Resolve]] и [[Reject]]

Если вы пошли читать мою статью про Promise там все про это расписано.

P.S Не стесняйтесь задавать вопросы

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