Пример: Есть три цепочки 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. Эти строки означают начало нового цикла микрозадач.