Почему JavaScript Promise, затем обработчик, запускается после другого кода?

Я просто пытаюсь улучшить свое понимание работы JavaScript Promises. Я создал следующую ситуацию:

LOG 'FOO'
RUN CALLBACK LOGGING 'CALLBACK'
LOG 'BAR'

Ожидайте, что все функции будут завершены немедленно (я имею в виду, что они не будут принимать чрезмерное/неизвестное количество времени, чтобы завершить выполнение операции async), чтобы вышеупомянутый порядок операций произошел в этом порядке.

Вы можете записать это следующим образом:

function foo(cb) {
  // LOG 'FOO'
  console.log('foo');
  // RUN CALLBACK
  cb();
}

function callback() {
  // LOG 'CALLBACK'
  console.log('callback');
}

foo(callback);

console.log('bar');

Это дает ожидаемый результат в соответствии с ситуацией, указанной в начале.

> foo
> callback
> bar

Вы также можете записать его следующим образом:

function foo() {
  return new Promise((resolve) => {
    // LOG 'FOO'
    console.log('foo');
    return resolve(null);
  });
}

function callback() {
  // LOG 'CALLBACK'
  console.log('callback');
}

foo().then(callback);

// LOG 'BAR'
console.log('bar');

Эта ситуация дает следующий результат:

> foo
> bar
> callback

Вот где я неясен, так как я ожидаю, что foo завершится немедленно, чтобы callback запустил и запустил 'callback' до bar logs 'bar'

Ответы

Ответ 1

Соответствующие спецификации здесь:

  • Promises/A + point 2.2.4:

    onFulfilled или onRejected не нужно вызывать, пока стек контекста выполнения не содержит только код платформы. [3,1].

    И обратите внимание на 3.1 (акцент мой):

    Здесь "код платформы" означает код реализации, среды и обещания. На практике это требование гарантирует, что onFulfilled и onRejected выполняются асинхронно, после того, как цикл цикла обработки событий, который затем вызывается, и со свежим стекем. Это может быть реализовано либо с помощью механизма макрозадачи, такого как setTimeout или setImmediate, либо с помощью механизма "микрозадания", такого как MutationObserver или process.nextTick. Поскольку реализация обещаний считается кодом платформы, она может сама содержать очередь планирования задач или "батут", в котором вызываются обработчики.

  • ECMAScript 6.0 (на основе Promises/A +) немного сложнее извлечь фрагменты, но then разрешает как в разделе 25.4.5.3.1:

    1. Иначе, если значение обещания [[PromiseState]] внутреннего слота "fulfilled",

      а. Пусть значение будет значением внутреннего слота [[PromiseResult]].

      б. Выполните EnqueueJob ("PromiseJobs", PromiseReactionJob, "performReaction, value" ).

    2. Иначе, если значение обещания [[PromiseState]] внутреннего слота "rejected",

      а. Пусть причина - это стоимость обещания [[PromiseResult]] внутреннего слота.

      б. Выполните EnqueueJob ("PromiseJobs", PromiseReactionJob, "rejectReaction, reason" ).

И важная операция EnqueueJob определена в разделе раздел 8.4 ( "Задания и очереди работы" ), показывая это в своем предисловии (жирным шрифтом является мой):

Выполнение задания может быть начато только в том случае, если не существует исполняемого контекста выполнения, а стек контекста выполнения пуст. [...] После запуска задания выполняется задание всегда выполняется до завершения. Ни одно другое задание не может быть начато до завершения текущего выполняемого задания.

На практике это позволяет сделать несколько простых и последовательных утверждений:

  • Вы можете рассчитывать на then или catch (и т.д.), чтобы всегда вести себя асинхронно, никогда не синхронно.
  • Вы никогда не увидите нескольких обработчиков then или catch в одном стеке, даже если одно обещание явно разрешено в рамках другого обещания. Это также означает, что рекурсивное выполнение Promise не рискует переполнением стека, поскольку может случиться обычный вызов функции, хотя вы все равно можете столкнуться с кучей пространства, если вы небрежны с рекурсивными замыканиями в патологическом случае.
  • Многопользовательские операции, поставленные в очередь в обработчике then или catch, никогда не будут блокировать текущий поток, даже если обещание уже установлено, поэтому вы можете поставить в очередь ряд асинхронных операций, не беспокоясь о порядке или обещании состояние.
  • В then или catch никогда не будет включен try, даже при вызове then на уже установленном Promise, поэтому нет никакой двусмысленности в отношении того, должна ли платформа обрабатывать возникшее исключение.

Ответ 2

Я действительно не хочу быть тупым, но это потому, что спецификация говорит, что они работают. Если вам нужно, чтобы часть кода запускалась в определенный момент после завершения кода в рамках обещания, вы должны использовать цепочку обещаний. Когда вы вводите асинхронный код в микс, неплохо было бы попытаться смешать его с зависимым, синхронным кодом.

Цепь Daisy promises, когда вам нужны вещи, зависящие от асинхронного кода:

function foo() {
    console.log('foo');
    return Promise.resolve();
}

function callback() {
    console.log('callback');
}

function consoler() {
    console.log('bar');
}

foo().then(callback).then(consoler);

Выдает:

foo
callback
bar

Ответ 3

Это невозможно из-за того, как работает promises. Даже promises, который немедленно разрешает запуск следующего тика, вам нужны функции синхронизации, а не promises.

Смотрите это, например:

setTimeout(function() {
    console.log("test");
}, 0);


console.log("test2");

Невозможно выполнить печать теста перед тестом2 без удаления функции setTimeout, потому что даже если параметр wait равен 0, он будет работать для следующего тика, что означает, что он будет работать, когда будет запущен весь код синхронизации.