Пример: Есть три цепочки promise. Если посмотреть на первый метод .then
каждой из них, то в первой возвращается промис, во второй - thenable
-объект, а в третьей - примитив. Попробуйте угадать, в каком порядке числа выведутся в консоль:
class Thenable {
then(resolve, reject) {
resolve();
}
}
new Promise(resolve => resolve())
.then(() => {
console.log(1);
return new Promise(resolve => resolve());
})
.then(() => console.log(2));
new Promise(resolve => resolve())
.then(() => {
console.log(3);
return new Thenable();
})
.then(() => console.log(4));
new Promise(resolve => resolve())
.then(() => console.log(5))
.then(() => console.log(6))
.then(() => console.log(7));
Если запустить этот код, порядок вывода будет следующий: 1, 3, 5, 6, 4, 7, 2.
- Но почему 4 выведется раньше, чем 2? Ведь
thenable
-объекты обрабатываются так же, как промисы..? (спойлер: не совсем) - И, что еще интереснее, почему 2 будет выведено в самом конце?
Чтобы ответить на эти вопросы, а также понять, как работает Promise.prototype.then
под капотом, давайте обратимся к спецификации ECMAScript.
Предупреждение: в коде ниже буду использованы термины из спецификации. Чтобы было проще в них ориентироваться, можете воспользоваться моей шпаргалкой.
Для начала выясним, как ведет себя метод promise.then
, возвращающий примитив.
Promise.resolve(1)
.then(result => console.log(result));
Перепишу этот код таким образом:
let p = Promise.resolve(1);
let callback = result => console.log(result); // (*)
p.then(callback);
А теперь объяснение того, что здесь происходит с помощью псевдокода на JS:
- Промис
p
разрешается сразу:
// FulfillPromise(p, 1)
p.[[PromiseResult]] = 1;
p.[[PromiseState]] = FULFILLED;
- Вызывается метод
.then(onFulfilled, onRejected)
:
1) При вызове метода Promise.prototype.then создается переменная promise:
let promise = this;
2) Далее алгоритм NewPromiseCapability создает объект resultCapability, содержащий специальный промис и его разрешающие функции:
let resultCapability = { [[Promise]]: __promise, [[Resolve]]: resolvingFunctions.[[Resolve]], [[Reject]]: resolvingFunctions.[[Reject]] };
/* promise и __promise - два разных промиса */
3) Затем возвращается вызов функции PerformPromiseThen:
return PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability);
/* В данном случае onFulfilled - это callback - (*), а onRejected - undefined */
- Выполняется алгоритм PerformPromiseThen:
// PerformPromiseThen(promise, onFulfilled, undefined, resultCapability)
let reaction = { [[Capability]]: resultCapability, [[Type]]: FULFILL, [[Handler]]: onFulfilled };
let value = promise.[[PromiseResult]]; // в value записывается 1
let fulfillJob = NewPromiseReactionJob(reaction, value); // (**)
HostEnqueuePromiseJob(fulfillJob);
- Выполняется алгоритм NewPromiseReactionJob -
(**)
. Он создает колбэк-функцию, которая при вызове выполнит следующие шаги:
// NewPromiseReactionJob(reaction, value)
let fulfillJob = () => {
let handler = reaction.[[Handler]]; // в handler записывается callback - (*)
let resolveFn = reaction.[[Capability]].[[Resolve]]; // resolve-функция специального промиса
let handlerResult = handler(value); // в handlerResult записывается результат вызова callback(1)
return resolveFn(handlerResult); // (***)
}
- Выполняется алгоритм HostEnqueuePromiseJob. Он ставит задачу в очередь микрозадач:
// HostEnqueuePromiseJob(fulfillJob)
queueMicrotask(fulfillJob);
TICK // Выполнение текущей задачи (скрипта) завершено
- Наступает стадия выполнения микрозадач. Выполняется микрозадача
fulfillJob
:
В консоль выводится 1, затем специальный промис, созданный на 2 шаге, разрешается - (***).
На этом выполнение завершается.
P.S. Promise.prototype.then()
всегда возвращает специально созданный промис, именно поэтому вне зависимости от того, что вы возвращаете из метода .then
, можно составлять цепочки промисов любой длины.
Теперь рассмотрим, что произойдет, если вернуть из promise.then
другой промис.
Promise.resolve(1)
.then(result => {
console.log(result);
return new Promise(resolve => resolve(2));
});
Перепишу этот код иначе:
let p1 = Promise.resolve(1);
let p1_then_callback = result => {
console.log(result);
let p3 = new Promise(resolve => resolve(2));
return p3;
};
let p2 = p1.then(p1_then_callback);
Объяснение:
- Промис
p1
разрешается сразу:
// FulfillPromise(p1, 1)
p1.[[PromiseResult]] = 1;
p1.[[PromiseState]] = FULFILLED;
- Вызывается метод
.then
, выполняется алгоритм PerformPromiseThen:
// PerformPromiseThen(p1, p1_then_callback, undefined, resultCapability)
let reaction = { [[Capability]]: resultCapability, [[Type]]: FULFILL, [[Handler]]: p1_then_callback }
/* resultCapability - как и в первом примере, специально созданный промис со свойствами [[Promise]], [[Resolve]] и [[Reject]] */
let value = p1.[[PromiseResult]];
let fulfillJob = NewPromiseReactionJob(reaction, value);
HostEnqueuePromiseJob(fulfillJob);
- Выполняется алгоритм NewPromiseReactionJob. Он создает колбэк-функцию, которая при вызове выполнит следующие шаги:
// NewPromiseReactionJob(reaction, value)
let fulfillJob = () => {
let handler = reaction.[[Handler]]; // handler - это p1_then_callback
let resolveFn = reaction.[[Capability]].[[Resolve]]; // resolve-функция специального промиса
let handlerResult = handler(value); // вызывается p1_then_callback(1) и возвращается промис p3
return resolveFn(handlerResult);
}
- Выполняется алгоритм HostEnqueuePromiseJob. Он ставит задачу в очередь микрозадач:
// HostEnqueuePromiseJob(fulfillJob)
queueMicrotask(fulfillJob);
TICK // Выполнение задачи (скрипта) завершено. Наступает стадия выполнения микрозадач. Выполняется fulfillJob
- Промис
p3
разрешается:
// FulfillPromise(p3, 2)
p3.[[PromiseResult]] = 2;
p3.[[PromiseState]] = FULFILLED;
- Для выполнения последней строки функции
fulfillJob
используется алгоритм Promise Resolve Functions:
Пояснение: специально созданный во 2 шаге промис разрешиться сразу не может, так как в resolveFn
, отвечающей за его разрешение, передан промис.
// Вызывается разрешающая функция специального промиса: resolve(handlerResult)
let promise = reaction.[[Capability]].[[Promise]]; // это сам специально созданный во 2 шаге промис
let resolution = handlerResult; // это промис p3
let thenJobCallback = { [[Callback]]: resolution.then };
let job = newPromiseResolveThenableJob(promise, resolution, thenJobCallback) // (*)
HostEnqueuePromiseJob(job); // queueMicrotask(job)
- Выполняется алгоритм NewPromiseResolveThenableJob -
(*)
. Он создает колбэк-функцию, которая при вызове выполнит следующие шаги:
// NewPromiseResolveThenableJob(promiseToResolve, thenable, then)
let job = () => {
let resolvingFunctions = CreateResolvingFunctions(promiseToResolve);
/* CreateResolvingFunctions возвращает объект со свойствами [[Resolve]] и [[Reject]] - разрешающими функциями promiseToResolve */
/* Это те же функции, которые были записаны в специально созданный промис на 2 шаге, но они создаются заново */
let thenCallResult = then.[[Callback]].call(thenable, resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]]);
/* Можно упростить: let thenCallResult = thenable.then(resolvingRunction.[[Resolve]], resolvingFunctions.[[Reject]]) */
return thenCallResult;
}
- Выполняется алгоритм HostEnqueuePromiseJob. Он ставит задачу в очередь микрозадач:
// HostEnqueuePromiseJob(job)
queueMicrotask(job);
TICK // Микрозадача fulfillJob выполнена. Достается старейшая задача из очереди микрозадач, это job
- У
thenable
(промисp3
) вызывается метод.then
, выполняется алгоритм PerformPromiseThen:
// PerformPromiseThen(p3, onFulfilled, onRejected, resultCapability_2)
let reaction_2 = { [[Capability]]: resultCapability_2, [[Type]]: FULFILL, [[Handler]]: onFulfilled }
let value_2 = p3.[[PromiseResult]]; // value_2 становится равным 2
let fulfillJob_2 = NewPromiseReactionJob(reaction_2, value_2); // (**)
HostEnqueuePromiseJob(fulfillJob_2); // queueMicrotask(fulfillJob_2)
- Выполняется алгоритм NewPromiseReactionJob -
(**)
. Как и в шаге 3, создается колбэк-функция, которая при вызове выполнит следующие шаги:
// NewPromiseReactionJob(reaction, value)
let fulfillJob_2 = () => {
let handler_2 = reaction.[[Handler]];
let resolveFn_2 = reaction.[[Capability]].[[Resolve]];
let handlerResult_2 = handler_2(value); // (***)
return resolveFn_2(handlerResult_2);
}
- Выполняется алгоритм HostEnqueuePromiseJob. Он ставит задачу в очередь микрозадач:
// HostEnqueuePromiseJob(fulfillJob_2)
queueMicrotask(fulfillJob_2);
TICK // Микрозадача job выполнена. Достается старейшая задача из очереди микрозадач, это fulfillJob_2
- При вызове
handler_2
-(***)
специально созданныйreaction.[[Capability]].[[Promise]]
разрешается с аргументом2
. Если бы на промисеp2
"висел" обработчик.then
, он был бы добавлен в очередь микрозадач.
P.S. В последней строке функции fulfillJob_2
разрешается еще один специальный промис reaction_2.[[Capability]].[[Promise]]
- это промис, созданный для обработчика p3.then
.
Что произойдет, если из promise.then
вернуть thenable
-объект.
class Thenable {
constructor(value) {
this.value = value;
}
then(resolve, reject) {
resolve(this.value);
}
}
new Promise(resolve => resolve())
.then(() => {
console.log(1);
return new Thenable(2);
});
На самом деле этот пример очень схож с предыдущим, потому что последовательность действий будет такая же. Единственное отличие - шаги 9-11 будут пропущены, так как алгоритм PerformPromiseThen выполняется только для промисов, а экземпляр класса Thenable
- обычный объект.
Надеюсь, поведение методов .then
в промисах стало немного яснее. И теперь понятно, почему в изначальном примере:
- При выполнении
.then
, возврающего промис, успевает выполниться три.then
, возвращающих примитивное значение. - А при выполнении
.then
, возвращающегоthenable
-объект, - только два.
Чтобы в этом окончательно убедиться, сравните в 1 и 2 примерах число строк с TICK
. Эти строки означают начало нового цикла микрозадач.