Антон Ларичев
Event Loop в JavaScript — как работает цикл событий
Введение
Event Loop (цикл событий) — это механизм, который позволяет JavaScript выполнять асинхронный код, несмотря на то что язык однопоточный. Понимание Event Loop — это то, что отличает разработчика, который просто «использует async/await», от того, кто понимает, что происходит под капотом.
Это одна из самых популярных тем на собеседованиях по JavaScript, и знание её помогает избежать целого класса трудноуловимых багов в асинхронном коде.
Однопоточность JavaScript
JavaScript — однопоточный язык. Это значит, что в один момент времени выполняется только одна инструкция. В браузере один поток отвечает за выполнение JavaScript, отрисовку страницы и обработку пользовательских событий.
На первый взгляд это противоречит тому, что мы делаем: отправляем запросы к серверу, ставим таймеры, обрабатываем клики — всё одновременно. Как это работает?
Ответ: Event Loop + Web APIs браузера (или Node.js APIs в серверном окружении).
Компоненты Event Loop
Call Stack (стек вызовов)
Call Stack — это структура данных, которая отслеживает выполнение функций. Работает по принципу LIFO (Last In, First Out — последним вошёл, первым вышел).
function greet(name) {
console.log(`Привет, ${name}!`);
}
function main() {
greet('Иван');
}
main();
Порядок выполнения:
main()попадает в стек- Внутри
main()вызываетсяgreet('Иван')— попадает в стек - Внутри
greet()вызываетсяconsole.log(...)— попадает в стек console.logзавершился — выходит из стекаgreetзавершился — выходит из стекаmainзавершился — выходит из стека
Если стек переполнится (бесконечная рекурсия), получим ошибку Maximum call stack size exceeded.
Web APIs
Браузер предоставляет API, которые работают вне JavaScript-движка: setTimeout, fetch, addEventListener, XMLHttpRequest, requestAnimationFrame и другие.
Когда вы вызываете setTimeout, браузер берёт функцию обратного вызова и сам следит за таймером. JavaScript-движок в это время свободен и продолжает выполнять другой код.
Task Queue (очередь задач / макрозадачи)
Когда Web API завершает работу (таймер сработал, запрос вернул ответ), callback-функция помещается в очередь задач. Event Loop берёт задачи из этой очереди только тогда, когда Call Stack полностью пуст.
console.log('1');
setTimeout(() => {
console.log('2'); // выполнится позже
}, 0);
console.log('3');
// Вывод: 1, 3, 2
Даже при setTimeout(fn, 0) — нулевой задержке — callback выполнится только после того, как весь синхронный код завершится.
Если вы хотите глубоко понять асинхронный JavaScript, включая Event Loop, промисы и async/await — приходите на наш курс JavaScript с нуля. На курсе 218 уроков и 80 упражнений, AI-тренажёры для практики 24/7, решение задач с живым ревью наставника, еженедельные встречи с менторами.
Microtask Queue (очередь микрозадач)
Микрозадачи — это отдельная очередь с более высоким приоритетом, чем обычные задачи. В неё попадают:
- Обработчики
.then(),.catch(),.finally()у промисов await-выраженияqueueMicrotask()MutationObserver
Ключевое правило: после каждой макрозадачи (и перед следующей) Event Loop полностью опустошает очередь микрозадач.
console.log('1');
setTimeout(() => console.log('2'), 0); // макрозадача
Promise.resolve().then(() => console.log('3')); // микрозадача
console.log('4');
// Вывод: 1, 4, 3, 2
Порядок: синхронный код (1, 4) → микрозадачи (3) → макрозадачи (2).
Полная картина Event Loop
Алгоритм работы Event Loop:
- Выполнить всё в Call Stack (синхронный код)
- Опустошить Microtask Queue (выполнить все микрозадачи, включая те, что добавляются во время выполнения микрозадач)
- Взять одну задачу из Task Queue (если стек пуст)
- Перейти к шагу 2
console.log('start'); // 1. синхронно
setTimeout(() => { // 4. макрозадача
console.log('setTimeout');
Promise.resolve().then(() => { // 5. микрозадача внутри макрозадачи
console.log('promise inside setTimeout');
});
}, 0);
Promise.resolve()
.then(() => {
console.log('promise 1'); // 2. микрозадача
return Promise.resolve();
})
.then(() => console.log('promise 2')); // 3. микрозадача (добавляется при выполнении шага 2)
console.log('end'); // 1. синхронно
// Вывод:
// start
// end
// promise 1
// promise 2
// setTimeout
// promise inside setTimeout
Практические примеры
Почему setState в React асинхронный?
React батчирует обновления состояния и применяет их как микрозадачи (в React 18) или после синхронного кода. Это объясняет, почему state не изменяется немедленно после setState.
Избегание блокировки UI
Тяжёлые вычисления блокируют Call Stack и делают UI неотзывчивым:
// Плохо — блокирует UI
function heavyWork() {
for (let i = 0; i < 1_000_000_000; i++) {
// что-то вычисляем
}
}
// Лучше — разбить на части через setTimeout
function heavyWorkChunked(items, chunkSize = 1000) {
let index = 0;
function processChunk() {
const end = Math.min(index + chunkSize, items.length);
for (; index < end; index++) {
// обрабатываем items[index]
}
if (index < items.length) {
setTimeout(processChunk, 0); // даём UI обновиться
}
}
processChunk();
}
Порядок выполнения async/await
async/await — это синтаксический сахар над промисами. Каждый await создаёт микрозадачу:
async function first() {
console.log('first: начало');
await Promise.resolve(); // создаёт микрозадачу
console.log('first: после await'); // выполнится как микрозадача
}
async function second() {
console.log('second: начало');
await Promise.resolve();
console.log('second: после await');
}
first();
second();
console.log('синхронный код');
// Вывод:
// first: начало
// second: начало
// синхронный код
// first: после await
// second: после await
requestAnimationFrame
requestAnimationFrame — особый тип задачи, выполняется перед отрисовкой кадра (обычно 60 раз в секунду). Он стоит между макрозадачами и отрисовкой в модели Event Loop:
// Выполнится прямо перед следующим кадром отрисовки
requestAnimationFrame(() => {
element.style.transform = 'translateX(100px)';
});
Это лучше, чем setTimeout для анимаций, потому что синхронизировано с циклом отрисовки браузера.
Частые ошибки
Предположение, что
setTimeout(fn, 0)выполнится немедленно. Даже нулевой таймаут — это макрозадача. Весь синхронный код и все микрозадачи выполнятся раньше.Бесконечный цикл в микрозадачах. Если в
.then()создавать новый промис и снова добавлять в очередь, Event Loop никогда не перейдёт к макрозадачам и интерфейс заморозится.Ожидание промиса без
awaitили.then().fetch('/api/data')сам по себе не ждёт — промис выполняется в фоне, а код продолжает синхронно.Смешение промисов и callback-стиля (старый способ с
setTimeoutвнутри промисов) без понимания порядка выполнения приводит к трудноотлаживаемым багам.
Часто задаваемые вопросы
Чем отличается Node.js Event Loop от браузерного?
Node.js имеет более сложную структуру с несколькими фазами: timers, pending callbacks, idle/prepare, poll, check (setImmediate), close callbacks. Однако базовые принципы (Call Stack, микрозадачи, макрозадачи) аналогичны браузерному.
Что такое queueMicrotask()?
Это явный способ добавить функцию в очередь микрозадач без создания промиса: queueMicrotask(() => console.log('микрозадача')). Полезно для библиотек и полифилов.
Можно ли заблокировать Event Loop?
Да. Любой тяжёлый синхронный код (сортировка миллиона элементов, сложные вычисления) блокирует Call Stack и замораживает UI. Для CPU-интенсивных задач в браузере используют Web Workers — отдельные потоки выполнения.
Почему промисы лучше callback?
Промисы используют микрозадачи (более высокий приоритет), что означает более предсказуемый порядок выполнения. Callback через setTimeout — макрозадача с меньшим приоритетом. Кроме того, промисы решают «callback hell» и упрощают обработку ошибок.
Что происходит при await внутри цикла?
// Последовательно — каждая итерация ждёт предыдущую
for (const id of ids) {
const data = await fetch(`/api/${id}`);
}
// Параллельно — все запросы запускаются одновременно
const results = await Promise.all(ids.map(id => fetch(`/api/${id}`)));
Заключение
Event Loop — фундаментальный механизм JavaScript. Его понимание объясняет поведение async/await, промисов, таймеров и пользовательских событий. Зная порядок выполнения микрозадач и макрозадач, вы можете точно предсказывать поведение асинхронного кода и избегать тонких ошибок.
Для закрепления знаний по асинхронному JavaScript рекомендуем курс JavaScript с нуля. В первых 3 модулях курса доступно бесплатное содержание — это позволяет познакомиться с подходом к обучению и понять структуру до покупки полного доступа.
Постройте личный план изучения Javascript до уровня Middle — бесплатно!
Javascript — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Javascript
Лучшие курсы по теме

Основы JavaScript
Антон Ларичев
TypeScript с нуля
Антон Ларичев