Задать вопрос

Почему именно такой порядок вывода в консоль?

new Promise((resolve) => resolve()) // P1
  .then(() => { // C1
    console.log(1);
    return new Promise((resolve) => resolve());
  })
  .then(() => console.log(2)); // C2

new Promise((resolve) => resolve()) // P2
  .then(() => console.log(3)) // C3
  .then(() => console.log(4)) // C4
  .then(() => console.log(5)) // C5
  .then(() => console.log(6)); // C6


Ответ: 1, 3, 4, 5, 2, 6.

Почему 4, 5 успевают проскочить, а 6 нет?
Я понимаю, что когда из then возвращается промис, создаётся микрозадача чтоб его обработать, но не понимаю, почему вообще успевают проскочить в консоль 4, 5 перед 2.
Для удобства обсуждения, в комментариях к коду написал краткие аббревиатуры.
  • Вопрос задан
  • 3076 просмотров
Подписаться 6 Сложный 1 комментарий
Решения вопроса 1
sergiks
@sergiks Куратор тега JavaScript
♬♬
Исследования в первоначальной версии ответа (ниже) натолкнули на поведение, которое смог объяснить Jonas Wilms в ответе на SO.

Возврат промиса из C1 then() создаёт ТРИ последовательных микротаска:
  1. NewPromiseResolveThenableJob, который вызывает then() на том уже отресолвленном промисе, что вернули в C1. И т.к. этот промис уже отресолвлен, сразу же в очередь вставляется следующий микротаск:
  2. NewPromiseReactionJob этого уже разрешенного промиса, и этот, в свою очередь, добавляет третью микрозадачу:
  3. ещё одну NewPromiseReactionJob, которая уже выводит в лог "2".


Если возвращать не промис (и не thenable объект), а "простое" значение или undefined, то добавляется всего один микротаск.

Раньше подобная задержка в три шага была и для await, но потом её оптимизировали в движке V8. А вот про then() забыли.

Подробнее стоит посмотреть ответ на SO, ссылка в начале.

[первая версия ответа]

Интересный вопрос! Больше экспериментов.
Вспомогательная функция для читаемости:
const log = (value, returnPromise) => () => {
  console.log(value);
  if (returnPromise) return Promise.resolve();
};
Возвращает функцию, которая обычный console.log() с переданным значением. И если второй аргумент трушный, то вернёт отрезолвленный промис.

Эксперимент 1. «Застёжка-молния»
Promise.resolve()
  .then(log('a1'))
  .then(log('a2'))
  .then(log('a3'))
  .then(log('a4'))
  .then(log('a5'))
  .then(log('a6'))
;

Promise.resolve()
  .then(log('b1'))
  .then(log('b2'))
  .then(log('b3'))
  .then(log('b4'))
  .then(log('b5'))
  .then(log('b6'))
;
Выводит поочередные a1 b1 a2 b2 a3 b3 ...
Эксперимент 2. «Отстаём на 2»

Единственное отличие: в первом А возвращаем отресолвленный промис.
Promise.resolve()
  .then(log('a1', true))
  .then(log('a2'))
  .then(log('a3'))
  .then(log('a4'))
  .then(log('a5'))
  .then(log('a6'))
;

Promise.resolve()
  .then(log('b1'))
  .then(log('b2'))
  .then(log('b3'))
  .then(log('b4'))
  .then(log('b5'))
  .then(log('b6'))
;

Выводит
a1 b1 b2 b3 a2 b4 a3 b5 a4 b6 a5 a6
А-шки после 1-й отстают на 2, пропустив вперёд b2 и b3.

Очередь микрозадач работает как FIFO буфер: первый пришёл, первый ушёл.

Цепочка из then() выполняется асинхронно. После выполнения очередного, создаётся следующий microtask. Несколько цепочек, как видно из 1-го эксперимента, выполняются параллельно-пошагово, «молнией».

Возврат созданного выполненного промиса, и ожидание его разрешения вызывает задержку в очереди микрозадач на 1 микротаск + «перемещение» цепочки "А" в конец (меняется порядок a-b => b-a):
"молния"
a1 b1 ; a2 b2 ; a3 b3 ; a4 b4 ; a5 b5 ; a6 b6

с промисом в А1
a1 b1 ; b2 ; b3 a2 ; b4 a3 ; b5 a4 ; b6 a5 ; a6


Подробнее с тремя цепочками

На этот раз три цепочки промисов "a", "b", "c". Цепочка "А" вернёт промис на 1-м шаге, "С" — на 4-м. Код эксперимента:
const output = [];

const makeChain = (key, n = 5, trueStep = false) => {
  let p = Promise.resolve();
  const arrow = isArrow => isArrow ? '->' : '';
  for (let i = 1; i <= n; i++) {
    const returnPromise = trueStep === i;
    const afterPromise = trueStep === i - 1;
    p = p.then(() => {
      output.push(`${arrow(afterPromise)}${key}${i}${arrow(returnPromise)}`);
      if (returnPromise) return Promise.resolve();      
    });
  }
  return p.catch(console.error);
};

const n = 7;
makeChain('a', n, 1),
makeChain('b', n),
makeChain('c', n, 4),

// мАкрозадача выполнится после всех мИкрозадач:
setTimeout(() => console.log(output.join(' ')));

Результат c вручную добавленными разбивками:
a1-> b1 c1 ; b2 c2 ; b3 c3 ->a2 ; b4 c4-> a3 ; b5 a4 ; b6 a5 ; b7 ->c5 a6 c6 ; a7 c7
Тут всё ещё не вполне мне понятен порядок после возврата промиса из C4.
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
@NikitaLikosov
Представим eventloop step by step
1. Мы синхронно выполнили код и получили 2 микротаска с вызовом 1 и вызовом 3
2. Микротаски вызвались и создали еще 2 микротаска с вызовом 4 и вот этим вот промисом return new Promise((resolve) => resolve());(функция в then его просто вернула, а в следующий then он должен попасть выполненным, так что, js его отдельно сначала должен выполнять )
3. После вызова прошлых микротасок мы получили микротаск с вызовом 5 и 2
4. Осталось вызвать микротаск с 6 и все
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы