← Назад к вопросам

Как Event Loop добавляет micro tasks в call stack?

1.7 Middle🔥 291 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Event Loop и micro tasks: как это работает

Основная концепция

Event Loop — это механизм в JavaScript, который управляет выполнением кода. Он постоянно проверяет, есть ли задачи в очередях (Call Stack, Callback Queue, Microtask Queue) и выполняет их по определённому порядку.

Структура Event Loop

Call Stack
├─ Синхронный код (выполняется сразу)
├─ Функции
├─ Операции
└─ Удаляется при return

Microtask Queue
├─ Promise callbacks (.then, .catch, .finally)
├─ async/await
├─ queueMicrotask()
├─ MutationObserver
└─ Выполняется после каждой синхронной операции

Callback Queue (Macrotask Queue)
├─ setTimeout
├─ setInterval
├─ setImmediate
├─ I/O операции
├─ UI events (click, scroll)
└─ Выполняется после microtask queue

Порядок выполнения

// 1. Выполняем весь синхронный код
console.log('1. Start');

// 2. Добавляем Promise в microtask queue
Promise.resolve()
  .then(() => console.log('2. Promise (microtask)'));

// 3. Добавляем setTimeout в callback queue
setTimeout(() => {
  console.log('4. setTimeout (macrotask)');
}, 0);

// 4. Продолжаем синхронный код
console.log('3. End');

// Вывод:
// 1. Start
// 3. End
// 2. Promise (microtask)
// 4. setTimeout (macrotask)

Почему Promise выполняется ДО setTimeout?

Потому что Event Loop сначала выполняет все microtasks, потом все macrotasks.

Этап 1: Выполняем Call Stack
  console.log('1. Start')   ✓
  console.log('3. End')     ✓

Этап 2: Call Stack пуст, проверяем Microtask Queue
  Promise.then()            ✓

Этап 3: Microtask Queue пуста, проверяем Callback Queue
  setTimeout()              ✓

Детальный процесс

console.log('Начало');

// Микротаск 1
Promise.resolve()
  .then(() => {
    console.log('Promise 1');
    
    // Если добавить микротаск ВО ВРЕМЯ микротаска
    return Promise.resolve()
      .then(() => console.log('Promise 1.1'));
  });

// Макротаск 1
setTimeout(() => {
  console.log('setTimeout 1');
  
  // Если добавить микротаск во время макротаска
  Promise.resolve()
    .then(() => console.log('Promise (в setTimeout)'));
}, 0);

// Микротаск 2
Promise.resolve()
  .then(() => console.log('Promise 2'));

console.log('Конец');

// Вывод:
// Начало
// Конец
// Promise 1
// Promise 1.1    ← Даже вложенный Promise выполнится перед setTimeout!
// Promise 2
// setTimeout 1
// Promise (в setTimeout)

Микротаски vs Макротаски

ТипПримерыКогда выполняется
MicrotaskPromise, async/await, queueMicrotask(), MutationObserverПосле каждого макротаска или синхронного кода
MacrotasksetTimeout, setInterval, setImmediate, I/O, DOM eventsПосле выполнения всех микротасков

Пример с async/await

// async/await это просто синтаксис для Promise

async function test() {
  console.log('1. In async');
  await Promise.resolve();
  console.log('2. After await');
}

console.log('0. Start');
test();
console.log('3. End');

// Вывод:
// 0. Start
// 1. In async
// 3. End
// 2. After await   ← Выполняется в микротаск очереди

Реальный пример: Race Condition

// ❌ Проблема: Event Loop может вызвать проблемы
let count = 0;

// Макротаск: setTimeout
setTimeout(() => {
  count++;
  console.log('setTimeout:', count);
}, 0);

// Микротаск: Promise
Promise.resolve().then(() => {
  count++;
  console.log('Promise:', count);
});

// Вывод:
// Promise: 1     ← Выполнится первым!
// setTimeout: 2

// ✅ Решение: используй один тип
Promise.resolve().then(() => {
  count++;
  console.log('Promise 1:', count);
});

Promise.resolve().then(() => {
  count++;
  console.log('Promise 2:', count);
});

// Вывод:
// Promise 1: 1
// Promise 2: 2
// Порядок предсказуем

Как добавлять микротаски вручную

// Способ 1: Promise
Promise.resolve()
  .then(() => console.log('Micro 1'));

// Способ 2: async/await
(async () => {
  await Promise.resolve();
  console.log('Micro 2');
})();

// Способ 3: queueMicrotask (явно)
queueMicrotask(() => {
  console.log('Micro 3');
});

// Способ 4: MutationObserver (legacy)
const observer = new MutationObserver(() => {
  console.log('Micro 4 (MutationObserver)');
});
observer.observe(document.body, { childList: true });
document.body.appendChild(document.createElement('div'));

Визуализация Event Loop

console.log('Шаг 1: Синхронный код');

setTimeout(() => {
  console.log('Шаг 5: Макротаск');
}, 0);

Promise.resolve()
  .then(() => {
    console.log('Шаг 3: Микротаск');
    
    // Вложенный микротаск
    queueMicrotask(() => {
      console.log('Шаг 4: Вложенный микротаск');
    });
  });

console.log('Шаг 2: Синхронный код (продолжение)');

// Timeline:
// t=0:   Выполняем синхронный код
//        Шаг 1
//        Шаг 2
// t=0+:  Проверяем Microtask Queue
//        Шаг 3
//        Шаг 4
// t=0++: Проверяем Callback Queue
//        Шаг 5

// Вывод:
// Шаг 1
// Шаг 2
// Шаг 3
// Шаг 4
// Шаг 5

Практическое применение: Batch обновления

// Пример: обновляем DOM, но хотим сделать это батчем

let updates = [];
let scheduled = false;

function scheduleUpdate(callback) {
  updates.push(callback);
  
  if (!scheduled) {
    scheduled = true;
    
    // Используем микротаск для батчинга
    queueMicrotask(() => {
      updates.forEach(cb => cb());
      updates = [];
      scheduled = false;
    });
  }
}

// Использование
scheduleUpdate(() => {
  console.log('Обновление 1');
});
scheduleUpdate(() => {
  console.log('Обновление 2');
});
scheduleUpdate(() => {
  console.log('Обновление 3');
});

// Все выполнятся в одной микротаск очереди
// Обновление 1
// Обновление 2
// Обновление 3

Ошибки с Event Loop

// ❌ Ошибка: забыли про микротаски
function process() {
  // Пользователь ждёт синхронный результат
  return new Promise(resolve => {
    resolve('data');
  });
}

const result = process(); // ❌ undefined! (Promise, а не данные)

// ✅ Правильно
process().then(result => {
  console.log(result); // 'data'
});

// ❌ Ошибка: setTimeout вместо микротаска
function notify() {
  setTimeout(() => {
    // Может быть очень задержано
    console.log('notification');
  }, 0);
}

// ✅ Правильно: микротаск для уведомлений
function notify() {
  queueMicrotask(() => {
    console.log('notification'); // Сразу после синхронного кода
  });
}

Итоги

Event Loop работает так:

1. Выполняй весь синхронный код (Call Stack)
2. Когда Call Stack пуст:
   a. Выполняй ВСЕ микротаски (Microtask Queue)
   b. Выполняй ОДИН макротаск (Callback Queue)
   c. Перейти к пункту 2

Микротаски ВСЕГДА выполняются перед макротасками, поэтому Promise выполнится раньше setTimeout, даже если setTimeout имеет задержку 0.