Вопрос: Есть две цепочки 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 может быть передано что угодно; опишу базовые кейсы:
resolve("hi")
- передается примитив, таким образом это соответствует шагу 8 этого алгоритма, далее такой resolve будет прямо здесь же разрешен и promise получит состояние fulfilled со значением "hi". Все это произойдет до того как синхронный код будет прочитан полностью.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.
Promise.resolve("hi")
- передается примитив, и это соответствует шагам 2, 3, 4. Резонный вопрос: соответствуют ли эти шаги классическому resolve? Увы, но не совсем. Классический resolve озадачен лишь тем чтобы разрешить promise, а вот Promise.resolve обязан создать оболочку promise. Только 3ий шаг соответствует классическому resolve.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 нечего
Стек микрозадач: []
Стек макрозадач: []
Все :)
Спасибо за такое разъяснение, очень пригодилось. Только один момент не понял: #task1 всегда так отрабатывает? Создает какую-то совместимость? Не очень понятно почему этот шаг нужен. Я про эти строчки