Skip to content

Instantly share code, notes, and snippets.

@sdrjs
Last active September 22, 2024 08:07
Show Gist options
  • Save sdrjs/ac1c15e62dc74c96f0b093d190c78cb6 to your computer and use it in GitHub Desktop.
Save sdrjs/ac1c15e62dc74c96f0b093d190c78cb6 to your computer and use it in GitHub Desktop.
Что происходит, если из promise.then вернуть другой промис?

Пример: Есть три цепочки 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.

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

Пример 1

Для начала выясним, как ведет себя метод 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:

  1. Промис p разрешается сразу:
// FulfillPromise(p, 1)
p.[[PromiseResult]] = 1;
p.[[PromiseState]] = FULFILLED;
  1. Вызывается метод .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 */
  1. Выполняется алгоритм 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);
  1. Выполняется алгоритм 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); // (***)
}
  1. Выполняется алгоритм HostEnqueuePromiseJob. Он ставит задачу в очередь микрозадач:
// HostEnqueuePromiseJob(fulfillJob)
queueMicrotask(fulfillJob);
TICK // Выполнение текущей задачи (скрипта) завершено
  1. Наступает стадия выполнения микрозадач. Выполняется микрозадача fulfillJob:
В консоль выводится 1, затем специальный промис, созданный на 2 шаге, разрешается - (***).

На этом выполнение завершается.

P.S. Promise.prototype.then() всегда возвращает специально созданный промис, именно поэтому вне зависимости от того, что вы возвращаете из метода .then, можно составлять цепочки промисов любой длины.

Пример 2

Теперь рассмотрим, что произойдет, если вернуть из 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);

Объяснение:

  1. Промис p1 разрешается сразу:
// FulfillPromise(p1, 1)
p1.[[PromiseResult]] = 1;
p1.[[PromiseState]] = FULFILLED;
  1. Вызывается метод .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);
  1. Выполняется алгоритм 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);
}
  1. Выполняется алгоритм HostEnqueuePromiseJob. Он ставит задачу в очередь микрозадач:
// HostEnqueuePromiseJob(fulfillJob)
queueMicrotask(fulfillJob);
TICK // Выполнение задачи (скрипта) завершено. Наступает стадия выполнения микрозадач. Выполняется fulfillJob
  1. Промис p3 разрешается:
// FulfillPromise(p3, 2)
p3.[[PromiseResult]] = 2;
p3.[[PromiseState]] = FULFILLED;
  1. Для выполнения последней строки функции 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)
  1. Выполняется алгоритм 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;
}
  1. Выполняется алгоритм HostEnqueuePromiseJob. Он ставит задачу в очередь микрозадач:
// HostEnqueuePromiseJob(job)
queueMicrotask(job);
TICK // Микрозадача fulfillJob выполнена. Достается старейшая задача из очереди микрозадач, это job
  1. У 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)
  1. Выполняется алгоритм 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);
}
  1. Выполняется алгоритм HostEnqueuePromiseJob. Он ставит задачу в очередь микрозадач:
// HostEnqueuePromiseJob(fulfillJob_2)
queueMicrotask(fulfillJob_2);
TICK // Микрозадача job выполнена. Достается старейшая задача из очереди микрозадач, это fulfillJob_2
  1. При вызове handler_2 - (***) специально созданный reaction.[[Capability]].[[Promise]] разрешается с аргументом 2. Если бы на промисе p2 "висел" обработчик .then, он был бы добавлен в очередь микрозадач.

P.S. В последней строке функции fulfillJob_2 разрешается еще один специальный промис reaction_2.[[Capability]].[[Promise]] - это промис, созданный для обработчика p3.then.

Пример 3

Что произойдет, если из 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 в промисах стало немного яснее. И теперь понятно, почему в изначальном примере:

  1. При выполнении .then, возврающего промис, успевает выполниться три .then, возвращающих примитивное значение.
  2. А при выполнении .then, возвращающего thenable-объект, - только два.

Чтобы в этом окончательно убедиться, сравните в 1 и 2 примерах число строк с TICK. Эти строки означают начало нового цикла микрозадач.

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