Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как макротаски попадают в очередь
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, браузер не выполняет её сразу. Вместо этого браузер:
- Записывает таймер с указанной задержкой
- Когда время истекает, добавляет callback в очередь макротаск
- 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
Что происходит:
- Выполняется весь синхронный код (console.log 1, 4)
- setTimeout callback добавляется в очередь макротаск
- Promise.then добавляется в очередь микротаск
- Стек вызовов очищен
- Event Loop проверяет: есть ли микротаски? Да → выполняем все микротаски (выводим 3)
- 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
Пошаговое выполнение:
- Синхронный код: "Start", затем регистрируем setTimeout 1, Promise, setTimeout 2, "End"
- Стек очищен, проверяем микротаски: "Promise 1", "Promise 2"
- Берём первую макротаску: setTimeout 1 → выполняем, добавляем Promise в очередь микротаск
- Проверяем микротаски: "Promise inside setTimeout"
- Берём следующую макротаску: 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