Event Loop в JavaScript — как работает цикл событий

16 июня 2026
Автор

Антон Ларичев

Введение

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();

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

  1. main() попадает в стек
  2. Внутри main() вызывается greet('Иван') — попадает в стек
  3. Внутри greet() вызывается console.log(...) — попадает в стек
  4. console.log завершился — выходит из стека
  5. greet завершился — выходит из стека
  6. 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:

  1. Выполнить всё в Call Stack (синхронный код)
  2. Опустошить Microtask Queue (выполнить все микрозадачи, включая те, что добавляются во время выполнения микрозадач)
  3. Взять одну задачу из Task Queue (если стек пуст)
  4. Перейти к шагу 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 - && (и), || (или), ! (не)Деструктуризация в JavaScript — полное руководствоСтрелочка вправо

Постройте личный план изучения Javascript до уровня Middle — бесплатно!

Javascript — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Javascript

Как работает метод trim() - JavaScriptКак работает метод toUpperCase() - JavaScriptКак работает метод toLowerCase() - JavaScriptКак работает метод substring() - JavaScriptКак работает метод startsWith() - JavaScriptКак работает метод split() - JavaScriptКак работает метод slice() - JavaScriptКак работает метод search() - JavaScriptКак работает метод replaceAll() - JavaScriptКак работает метод replace() - JavaScriptКак работает метод repeat() - JavaScriptКак работает метод padStart() - JavaScriptКак работает метод padEnd() - JavaScriptКак работает метод matchAll() - JavaScriptКак работает метод match() - JavaScriptКак работает метод localeCompare() - JavaScriptКак работает свойство length - JavaScriptКак работает метод lastIndexOf() - JavaScriptКак работает метод indexOf() - JavaScriptКак работает метод includes() - JavaScriptКак работает метод fromCodePoint() - JavaScriptКак работает метод fromCharCode() - JavaScriptКак работает метод endsWith() - JavaScriptКак работает метод concat() - JavaScriptКак работает метод codePointAt() - JavaScriptКак работает метод charCodeAt() - JavaScriptКак работает метод charAt() - JavaScript
Итератор в JavaScript
try...catch в JavaScriptError в JavaScript
Событие wheel в JavaScriptСобытие unload в JavaScriptСобытие touch в JavaScriptСобытие submit в JavaScriptСобытие scroll в JavaScriptСобытие reset в JavaScriptМетод .preventDefault() в JavaScriptСобытие mouseover в JavaScriptСобытие mouseout в JavaScriptСобытие load в JavaScriptСобытие keyup в JavaScriptСобытие keydown в JavaScriptСобытие invalid в JavaScriptСобытие input в JavaScriptСобытийная модель Event в JavaScriptОбъект события Event в JavaScriptСобытие DOMContentLoaded в JavaScriptСобытие dblclick в JavaScriptСобытие click в JavaScriptСобытие change в JavaScriptСобытие beforeunload в JavaScript
Как работает метод some() - JavaScriptКак работает метод reverse() - JavaScriptКак работает метод reduce() - JavaScriptКак работает метод map() - JavaScriptКак работает метод isArray() - JavaScriptКак работает метод indexOf() - JavaScriptКак работает метод includes() - JavaScriptКак работает метод from() - JavaScriptКак работает метод forEach() - JavaScriptКак работает метод flatMap() - JavaScriptКак работает метод flat() - JavaScriptКак работает метод findIndex() - JavaScriptКак работает метод find() - JavaScriptКак работает метод filter() - JavaScriptКак работает метод every() - JavaScriptМассивы в JavaScript
Открыть базу знаний

Лучшие курсы по теме

изображение курса

Основы JavaScript

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий