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

Как макротаски попадают в очередь?

2.0 Middle🔥 161 комментариев
#JavaScript Core

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

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

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

Как макротаски попадают в очередь

Event Loop в JavaScript — это сердце асинхронного выполнения кода. Для понимания того, как работают макротаски (macrotasks), необходимо разобраться в концепции очереди задач, стека вызовов и механизме браузера, который управляет их выполнением.

Понятие макротаск и микротаск

JavaScript различает две типа асинхронных задач:

Макротаски (Macrotasks):

  • setTimeout, setInterval
  • setImmediate (Node.js)
  • requestAnimationFrame
  • fetch (после получения ответа)
  • Обработчики событий (click, input, etc.)
  • Парсинг HTML

Микротаски (Microtasks):

  • Promise.then/catch/finally
  • MutationObserver
  • Process.nextTick (Node.js)
  • queueMicrotask()

Ключевое отличие: все микротаски выполняются перед следующей макротаской!

Как макротаски попадают в очередь

Когда вы вызываете функцию типа setTimeout, браузер не выполняет её сразу. Вместо этого браузер:

  1. Записывает таймер с указанной задержкой
  2. Когда время истекает, добавляет callback в очередь макротаск
  3. Event Loop достаёт задачу из очереди и выполняет её
console.log('Start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

console.log('End');

// Вывод:
// Start
// End
// setTimeout

Хотя setTimeout установлен с задержкой 0, он всё равно выполняется после синхронного кода, потому что callback попадает в очередь макротаск.

Пошаговое выполнение: Event Loop

Разберу алгоритм Event Loop:

console.log('1. Synchronous code start');

setTimeout(() => {
  console.log('2. setTimeout callback');
}, 0);

Promise.resolve()
  .then(() => {
    console.log('3. Promise then');
  });

console.log('4. Synchronous code end');

// Вывод:
// 1. Synchronous code start
// 4. Synchronous code end
// 3. Promise then (микротаск выполняется раньше макротаска!)
// 2. setTimeout callback

Что происходит:

  1. Выполняется весь синхронный код (console.log 1, 4)
  2. setTimeout callback добавляется в очередь макротаск
  3. Promise.then добавляется в очередь микротаск
  4. Стек вызовов очищен
  5. Event Loop проверяет: есть ли микротаски? Да → выполняем все микротаски (выводим 3)
  6. Event Loop проверяет: есть ли макротаски? Да → выполняем одну (выводим 2)

Очередь макротаск в браузере

Браузер управляет несколькими очередями макротаск. Каждое событие попадает в соответствующую очередь:

// Таймеры
setTimeout(() => {
  console.log('setTimeout');
}, 0);

setInterval(() => {
  console.log('setInterval');
}, 1000);

// Обработчики событий
document.addEventListener('click', () => {
  console.log('click event');
});

// Fetch (после получения ответа)
fetch('/api/data').then(r => r.json()).then(data => {
  console.log('fetch complete'); // Это микротаск, не макротаск!
});

Каждый источник добавляет задачи в свою очередь. Event Loop обходит очереди и выполняет по одной задаче из каждой за раз.

Детальный пример: сложный Event Loop

console.log('Start');

// Макротаск 1: setTimeout
setTimeout(() => {
  console.log('setTimeout 1');
  
  Promise.resolve().then(() => {
    console.log('Promise inside setTimeout');
  });
}, 0);

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

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

console.log('End');

// Вывод:
// Start
// End
// Promise 1
// Promise 2
// setTimeout 1
// Promise inside setTimeout
// setTimeout 2

Пошаговое выполнение:

  1. Синхронный код: "Start", затем регистрируем setTimeout 1, Promise, setTimeout 2, "End"
  2. Стек очищен, проверяем микротаски: "Promise 1", "Promise 2"
  3. Берём первую макротаску: setTimeout 1 → выполняем, добавляем Promise в очередь микротаск
  4. Проверяем микротаски: "Promise inside setTimeout"
  5. Берём следующую макротаску: setTimeout 2

Как происходит добавление в очередь

// 1. Event попадает в очередь через API браузера
window.addEventListener('click', () => {
  console.log('clicked');
  
  // Эта функция добавляется в очередь макротаск после выполнения события
});

// 2. setTimeout добавляет задачу через браузерный таймер
const timerId = setTimeout(() => {
  console.log('timer fired');
  // Браузер добавляет эту callback в очередь макротаск когда время истекает
}, 1000);

// 3. requestAnimationFrame попадает в очередь для следующего кадра
requestAnimationFrame(() => {
  console.log('next frame');
  // Выполняется перед перерисовкой в следующем кадре
});

// 4. Fetch потребления: ответ добавляется через браузерный API
fetch('/api')
  .then(r => r.json()) // Это микротаск (Promise.then)
  .then(data => {
    console.log(data);
  });

Практический пример: визуализация Event Loop

function logWithTime(msg) {
  console.log(`[${Date.now() % 10000}] ${msg}`);
}

logWithTime('1. Script start');

setTimeout(() => {
  logWithTime('2a. setTimeout 1');
  
  Promise.resolve().then(() => {
    logWithTime('2b. Promise inside setTimeout');
  });
}, 0);

Promise.resolve()
  .then(() => {
    logWithTime('3a. Promise 1');
    
    setTimeout(() => {
      logWithTime('3b. setTimeout inside Promise');
    }, 0);
  });

requestAnimationFrame(() => {
  logWithTime('4. requestAnimationFrame');
});

logWithTime('5. Script end');

// Порядок:
// [0000] 1. Script start
// [0001] 5. Script end
// [0002] 3a. Promise 1
// [0003] 2a. setTimeout 1
// [0004] 2b. Promise inside setTimeout
// [0005] 4. requestAnimationFrame (может быть раньше)
// [0006] 3b. setTimeout inside Promise

Работа Event Loop: псевдокод

while (eventLoop.waitForTask()) {
  // 1. Выполнить одну макротаску
  const macrotask = macrotaskQueue.pop();
  if (macrotask) {
    execute(macrotask);
  }
  
  // 2. Выполнить все микротаски
  while (microtaskQueue.hasTasks()) {
    const microtask = microtaskQueue.pop();
    execute(microtask);
  }
  
  // 3. Если есть requestAnimationFrame, выполнить их
  if (isRepaintTime()) {
    runAnimationFrames();
  }
  
  // 4. Перерисовка страницы (repaint)
  repaint();
}

Специальные случаи

requestAnimationFrame — особая позиция:

setTimeout(() => {
  console.log('setTimeout');
}, 0);

requestAnimationFrame(() => {
  console.log('requestAnimationFrame');
});

// requestAnimationFrame выполняется перед перерисовкой,
// но после завершения текущей макротаски и всех микротаск

Вложенные setTimeout:

setTimeout(() => {
  console.log('setTimeout 1');
  
  setTimeout(() => {
    console.log('setTimeout 2 (nested)');
  }, 0);
}, 0);

// setTimeout 1
// setTimeout 2 (nested) — выполнится в следующей итерации Event Loop

Выводы

  • Макротаски: setTimeout, setInterval, события, fetch
  • Микротаски имеют приоритет: все микротаски выполняются перед следующей макротаской
  • Event Loop выполняет одну макротаску, затем все микротаски, затем перерисовка
  • Порядок: синхронный код → микротаски → первая макротаска → микротаски → вторая макротаска
  • setTimeout(fn, 0) — не выполняет fn немедленно, добавляет в очередь макротаск
  • Promise.then выполняется раньше, чем setTimeout, даже с задержкой 0
Как макротаски попадают в очередь? | PrepBro