Исследования в первоначальной версии ответа (ниже) натолкнули на поведение, которое смог объяснить Jonas Wilms в
ответе на SO.
Возврат промиса из C1 then()
создаёт ТРИ последовательных микротаска:
- NewPromiseResolveThenableJob, который вызывает
then()
на том уже отресолвленном промисе, что вернули в C1. И т.к. этот промис уже отресолвлен, сразу же в очередь вставляется следующий микротаск:
- NewPromiseReactionJob этого уже разрешенного промиса, и этот, в свою очередь, добавляет третью микрозадачу:
- ещё одну 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.