Event Loop в JavaScript: микрозадачи и макрозадачи с примерами

Event Loop в JavaScript — базовый механизм, который определяет, когда выполняется ваш код: синхронный, коллбэки, промисы, таймеры и обработчики событий. Понимание очередей микрозадач и макрозадач помогает писать плавные интерфейсы, избегать лагов и уверенно проходить собеседования.
Event Loop JavaScript: микрозадачи и макрозадачи — что это
JavaScript однопоточен: в каждый момент времени исполняется одна операция. Остальные «ждут» своей очереди в специальных очередях задач, которыми управляет Event Loop.
- Микрозадачи (microtasks): коллбэки
Promise.then/catch/finally
,queueMicrotask
,MutationObserver
. Выполняются сразу после текущего стека синхронного кода и до следующей макрозадачи и перерисовки. - Макрозадачи (macrotasks):
setTimeout
,setInterval
, обработчики DOM-событий,MessageChannel
, сетевые коллбэки и т. п. Выполняются после завершения микрозадач и возможной перерисовки.
Общий цикл: синхронный код → все микрозадачи → возможно рендер → одна макрозадача → снова микрозадачи → рендер → ...
Порядок выполнения: наглядный пример
console.log('A');
setTimeout(() => console.log('B setTimeout'), 0);
Promise.resolve().then(() => console.log('C microtask: Promise.then'));
queueMicrotask(() => console.log('D microtask: queueMicrotask'));
console.log('E');
Вывод будет: A, E, C, D, B. Сначала — синхронные A
, E
, потом микрозадачи (C
, затем D
в порядке постановки), и лишь затем макрозадача от setTimeout
.
Async/await и микрозадачи
await
ставит продолжение функции в очередь микрозадач (через Promise
).
async function demo() {
console.log('1');
await null; // то же, что await Promise.resolve(null)
console.log('2');
}
console.log('0');
demo();
console.log('3');
Порядок: 0, 1, 3, 2. После await
выполнение demo «уходит» в микрозадачу, которая сработает после завершения текущего синхронного стека.
Практика: как не блокировать интерфейс
Длинные циклы и тяжелые вычисления «замораживают» UI, ведь главный поток занят. Решение — разбивать работу на порции и уступать управление циклу событий, позволяя браузеру отрисовать кадр.
Плохой пример
// Блокирует поток, интерфейс подвисает
for (let i = 0; i < 1e8; i++) {
// тяжёлая работа
}
console.log('Готово');
Разбиение на чанки + уступаем кадр
// Уступаем отрисовке через requestAnimationFrame
function nextFrame() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
async function processInChunks(items, chunkSize = 1000) {
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
// обработка порции
for (const x of chunk) {
// ... работа с x
}
// уступаем управление, чтобы браузер успел отрисовать кадр
await nextFrame();
}
console.log('Готово без фризов');
}
Почему не microtask для этого? Микрозадачи выполняются до отрисовки. Если вставлять await Promise.resolve()
в цикл, вы не дадите браузеру «вдохнуть». Используйте requestAnimationFrame
или хотя бы setTimeout(..., 0)
для уступки следующей макрозадаче.
Уступка через setTimeout и MessageChannel
// Макрозадача с минимальной задержкой
await new Promise(r => setTimeout(r, 0));
// Или MessageChannel — часто быстрее, чем setTimeout(0)
const ch = new MessageChannel();
ch.port1.onmessage = () => console.log('macrotask via MessageChannel');
ch.port2.postMessage(null);
Частые ошибки и как их исправить
-
Бесконечные микрозадачи «голодают» цикл.
function spin() { Promise.resolve().then(spin); // микрозадача снова ставит микрозадачу } spin(); // интерфейс перестанет отрисовываться
Исправление — периодически отдавайте управление через макрозадачу:
function spinSafe() { setTimeout(spinSafe, 0); // макрозадача даёт шанс отрисовке и другим задачам } spinSafe();
-
async в forEach не «ждёт» внутри цикла.
const items = [1,2,3]; items.forEach(async (x) => { await doWork(x); }); console.log('Готово?'); // выведется раньше, чем doWork завершится
Правильно — использовать
for...of
сawait
илиPromise.all
:// Последовательно for (const x of items) { await doWork(x); } console.log('Готово!'); // Параллельно await Promise.all(items.map(doWork)); console.log('Готово параллельно!');
-
Непонимание приоритета: почему Promise быстрее setTimeout?
Потому что
then
— микрозадача, которая выполняется до макрозадач. Запомните: «Promises first, timers later».
Короткая шпаргалка по Event Loop
- Сначала весь синхронный код, затем все микрозадачи, потом рендер и лишь затем следующая макрозадача.
- Микрозадачи:
Promise.then/catch/finally
,queueMicrotask
,MutationObserver
. - Макрозадачи:
setTimeout
,setInterval
,MessageChannel
, DOM-события, сетевые коллбэки. await
продолжает функцию как микрозадачу.- Чтобы дать браузеру отрисовать интерфейс — используйте
requestAnimationFrame
или макрозадачу. - Не используйте бесконечные цепочки микрозадач — это «голодание» рендера.
Мини-викторина для собеседования
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
Promise.resolve().then(() => setTimeout(() => console.log(4), 0));
console.log(5);
Ответ: 1, 5, 3, 2, 4. Объяснение: синхронно — 1 и 5, затем микрозадачи — 3, затем макрозадачи в порядке постановки — 2, и потом 4 (таймер, добавленный из микрозадачи).
Небольшое отличие в Node.js
В Node.js есть ещё process.nextTick
— спец. очередь, выполняется раньше промисов (ещё «более микро»). Для браузера достаточно помнить базовое правило о микро- и макрозадачах, а в Node — аккуратно использовать nextTick
, чтобы не блокировать цикл событий.
Что дальше изучать
Закрепите тему на практике: поэкспериментируйте с разными комбинациями Promise
, setTimeout
, MessageChannel
, попробуйте «распилить» тяжёлую задачу с уступкой кадра. А если хотите пройти структурированный путь от основ до продвинутых тем (включая асинхронность, работу с DOM и проектные практики), рекомендую практический курс «JavaScript с Нуля до Гуру 2.0» — разобраться и прокачаться на реальных задачах.
-
-
Михаил Русаков
Комментарии (0):
Для добавления комментариев надо войти в систему.
Если Вы ещё не зарегистрированы на сайте, то сначала зарегистрируйтесь.