Тонкости исполнения кода с микротасками

Я пытаюсь разобраться с последовательностью исполнения кода, когда в нём присутствуют микротаски. Я знаю, что в теории микротаски пушатся в очередь, когда стек вызовов пустой (т.е. когда на нём лежит только глобальный контекст). Например, у нас в глобальном контексте есть два последовательных for-цикла:

for(let i = 0; i<1000; i++){
    console.log(i);
};

for(let i = 0; i<1000; i++){
    console.log(i);
};

Правда ли, что до, после и во время исполнения кода циклов условие пустого стека выполняется? Если так, то почему микротаск (res)=> { console.log(res)} в коде ниже не выполняется между двумя последними циклами, если к тому времени он уже в microtask-очереди и стек вызовов пустой?

const promise = new Promise(function(resolve, _){
    resolve('microtask2');
});

for(let i = 0; i<1000; i++){
    console.log(i);
};

promise.then((res)=> {
    console.log(res);
)};

for(let i = 0; i<1000; i++){
    console.log(i);
};

for(let i = 0; i<1000; i++){
    console.log(i);
};

Вот примерно так я вижу порядок исполнения кода:

  1. Eexecutor-функция, передаваемая внутрь конструктора Promise(), вызывает метод resolve() и немедленно исполняет этот Promise.
  2. Выполняется первый цикл.
  3. Вызывается then(), его контекст помещается в стек вызовов (поверх глобального контекста).
  4. Колбэк внутри then() регистрируется где-то в среде браузера.
  5. Контекст then() убирается из стека.
  6. Выполняется еще один цикл, так как колбэк из then() только лишь зарегистрирован, и еще не находится в очереди (или он сразу попадает в очередь?).
  7. Теперь, когда цикл завершен и стек вызовов пуст, event loop берёт колбэк из очереди, перебрасывает его в стек вызовов и выполняет (я знаю, что это не так, и колбэк будет исполнен последним, но я не понимаю почему).
  8. Последний цикл выполняется К шагу 7 уже выполняются все условия для вызова колбэка:

а) в стеке ничего нет б) синхронный код в данный момент не выполняется в) колбэк ожидает в очереди

Почему колбэк находится в очереди до тех пор, пока не будет выполнен последний цикл, если между циклами есть множество "окошек" для выполнения?


Ответ на комментарий Alexey Ten:

Я сначала так и думал, что а первую очередь выполняется весь синхронный код, а уже затем проверяются очереди, но потом поигрался в IDE и получил код, где колбэк исполняются до того, как исполнится весь синхронный код:

const timer = setTimeout(function(){
    console.log('set timeout');
}, 0);

new Promise(function(resolve, _){
    try{   
        resolve('microtask1');
    }
    catch(err){
        console.error(`caught an ${err}`);
    }
}).then((res)=>{
    console.log(res);
});
console.log('synchronous code 1');

const result = await fetch(`https://jsonplaceholder.typicode.com/photos`);

console.log('synchronous code 2');

Если вы этот код запустите, то будет вот такой вывод:

введите сюда описание изображения

Т.е. строка синхронного кода запустилась после колбэков.

Возможно, дело в том, что выражение с await здесь использовано вне функции, как это было всегда, а на уровне глобального скоупа, как это стало возможным с ES2022 и есть какие-то особенности, которые я не учёл? На эту мысль меня наводит тот факт, что если await поместить в асинхронную функцию, то проблема исчезает и всё выводится на экран, в том порядке, в котором и ожидается:

let result;
const timer = setTimeout(function(){
    console.log('set timeout');
}, 0);

new Promise(function(resolve, _){
    try{   
        resolve('microtask1');
    }
    catch(err){
        console.error(`caught an ${err}`);
    }
}).then((res)=>{
    console.log(res);
});
console.log('synchronous code 1');

(async function(){
    result = await fetch(`https://jsonplaceholder.typicode.com/photos`);
})();

console.log('synchronous code 2');

Вывод на экран:

введите сюда описание изображения

Как думаете, связано ли это как-то со спецификой await?


Ответы (0 шт):