Акции и промокоды Отзывы о школах

Event Loop в JavaScript: простое объяснение работы цикла событий с примерами

#Блог

JavaScript — однопоточный язык программирования. Это означает, что он может выполнять только одну задачу в конкретный момент времени, поскольку располагает единственным потоком исполнения — таков фундаментальный принцип работы движка (например, V8 в Chrome или Node.js). Казалось бы, это серьёзное ограничение: как же тогда браузер умудряется обрабатывать клики пользователя, отправлять сетевые запросы и обновлять интерфейс одновременно, не превращаясь в неотзывчивое приложение?

Ответ кроется в механизме, который называется Event Loop — цикл событий. По сути, Event Loop — это сердце асинхронности в JavaScript. Он представляет собой алгоритм, который управляет порядком выполнения задач: следит за тем, что можно запустить прямо сейчас, а что должно подождать своей очереди. Благодаря Event Loop JavaScript способен делегировать «тяжёлые» операции — такие как таймеры, сетевые запросы или обработку событий DOM — окружению (браузеру или Node.js), не блокируя при этом основной поток выполнения программы.

Рассмотрим простой пример:

console.log('Начало');

setTimeout(() => {

console.log('Таймер');

}, 0);

console.log('Конец');

Интуитивно можно предположить, что раз задержка у setTimeout равна нулю, то «Таймер» выведется сразу после «Начало». Однако на практике мы увидим:

Начало

Конец

Таймер

Именно Event Loop объясняет, почему это происходит: синхронный код выполняется первым, а асинхронные задачи (даже с нулевой задержкой) попадают в очередь и обрабатываются позже. Этот механизм критически важен для создания отзывчивых пользовательских интерфейсов и эффективной работы серверных приложений — без понимания Event Loop разработчики рискуют столкнуться с неочевидными багами в поведении асинхронного кода.

Как устроена модель выполнения JavaScript-кода (runtime model)

Чтобы понять, как Event Loop управляет выполнением кода, необходимо разобраться в архитектуре JavaScript runtime — среды выполнения, которую предоставляет браузер или Node.js. Эта модель состоит из четырёх ключевых компонентов, каждый из которых выполняет свою роль в обработке синхронных и асинхронных задач:

  • Call Stack (Стек вызовов) — место, где непосредственно выполняется код.
  • Web APIs — окружение браузера, обрабатывающее асинхронные операции.
  • Task Queue (Очередь задач) — хранилище готовых к выполнению задач.
  • Event Loop — диспетчер, координирующий работу всех компонентов.

Давайте рассмотрим каждый элемент подробнее, чтобы увидеть полную картину того, как JavaScript справляется с асинхронностью, оставаясь однопоточным языком.

архитектура JavaScript Runtime.


Эта схема показывает, как взаимодействуют основные компоненты: Call Stack выполняет код, Web APIs обрабатывают асинхронные задачи, а Event Loop переносит готовые задачи из очередей обратно в стек.

Call Stack — стек вызовов

Call Stack представляет собой структуру данных типа LIFO (Last In, First Out — последним пришёл, первым ушёл), где JavaScript отслеживает, какие функции выполняются в данный момент и в каком порядке. Когда вы вызываете функцию, она добавляется в стек. Когда функция завершает работу, она удаляется из стека.

Принцип работы прост: JavaScript выполняет функции строго сверху вниз по стеку. Если одна функция вызывает другую, новая функция помещается поверх текущей — стек растёт. Движок обрабатывает их последовательно, и только когда верхняя функция закончит работу, управление передаётся следующей.

Рассмотрим пример:

function first() {

console.log('Первая');

second();

console.log('Первая завершена');

}

function second() {

console.log('Вторая');

}

first();

 

Здесь стек будет выглядеть так: сначала в него попадает first(), затем — second(). Как только second() завершается, она удаляется из стека, и выполнение возвращается к first(). Важно понимать: пока стек не пуст, JavaScript не переключается на другие задачи — это и есть суть синхронного выполнения.

Web APIs — окружение браузера

Web APIs — это набор интерфейсов, которые предоставляет окружение (браузер или Node.js), но которые не являются частью самого языка JavaScript. Сюда относятся механизмы для работы с таймерами (setTimeout, setInterval), обработки событий DOM (addEventListener), выполнения сетевых запросов (fetch, XMLHttpRequest) и многого другого.

Когда вы вызываете, например, setTimeout, сам JavaScript не ждёт истечения времени — он сразу передаёт эту задачу в Web APIs и продолжает выполнять следующий код. Окружение следит за таймером в фоновом режиме, и когда время истекает, колбэк из setTimeout попадает в очередь задач, откуда его заберёт Event Loop.

Именно благодаря Web APIs JavaScript остаётся неблокирующим: «тяжёлые» операции выполняются параллельно, не занимая основной поток.

Task Queue — очередь задач

Task Queue (также называемая Callback Queue или очередью макрозадач) — это место, куда помещаются задачи, готовые к выполнению после завершения своей асинхронной операции в Web APIs. Например, когда таймер отсчитал время или пришёл ответ от сервера, соответствующий колбэк попадает в эту очередь.

Очередь работает по принципу FIFO (First In, First Out — первым пришёл, первым ушёл): задачи выполняются в том порядке, в котором они туда попали. Однако важно понимать, что задача из очереди не может начать выполняться, пока Call Stack не освободится полностью.

Event Loop постоянно проверяет: «Стек пуст? Есть ли задачи в очереди?» Если ответ на оба вопроса положительный, он берёт первую задачу из Task Queue и помещает её в Call Stack для выполнения. Этот цикл повторяется бесконечно — отсюда и название «цикл событий».

Как работает Event Loop: пошаговое объяснение процесса

Event Loop можно представить как бдительного диспетчера, который никогда не спит и постоянно задаёт себе один вопрос: «Свободен ли Call Stack?» Если стек пуст, Event Loop проверяет очереди задач и, при наличии ожидающих задач, отправляет их на выполнение. Этот процесс повторяется итерация за итерацией, обеспечивая непрерывную работу приложения.

Базовый алгоритм работы Event Loop выглядит следующим образом:

  1. Выполнить весь синхронный код из текущего контекста.
  2. Проверить Call Stack — если он не пуст, продолжать выполнение.
  3. Когда стек освобождается, обработать все микрозадачи из очереди.
  4. Взять одну макрозадачу из очереди и выполнить её.
  5. Снова обработать все микрозадачи.
  6. Вернуться к шагу 2.

Разберём этот процесс на классическом примере с setTimeout:

console.log('Первый');

setTimeout(() => {

console.log('Второй');

}, 0);

console.log('Третий');

Пошагово происходит следующее: движок начинает выполнять код построчно. Сначала console.log(‘Первый’) попадает в Call Stack и немедленно выполняется — в консоли появляется «Первый». Затем встречается setTimeout — JavaScript не ждёт, а сразу передаёт его обработку в Web APIs с инструкцией: «Подожди 0 миллисекунд, потом отправь колбэк в очередь». Выполнение продолжается, и console.log(‘Третий’) выводит «Третий» в консоль.

На этом этапе весь синхронный код завершён, Call Stack пуст. Тем временем таймер в Web APIs истёк (на это ушло буквально мгновение), и колбэк () => console.log(‘Второй’) попал в Task Queue. Event Loop обнаруживает пустой стек и готовую задачу в очереди, забирает её и помещает в Call Stack. Только теперь выполняется console.log(‘Второй’), и в консоли появляется «Второй».

Итоговый вывод:

Первый

Третий

Второй

Этот пример наглядно демонстрирует ключевой принцип: даже если задержка у setTimeout равна нулю, колбэк не может выполниться раньше, чем завершится весь синхронный код и освободится стек.

Блок-схема алгоритма Event Loop.


Эта схема визуализирует процесс принятия решений, который непрерывно выполняет Event Loop: от проверки пустоты стека до обработки микрозадач и выполнения макрозадач.

Синхронные задачи

Синхронные задачи — это код, который выполняется немедленно и последовательно, строка за строкой. Сюда относятся обычные вызовы функций, операции с переменными, циклы, условные конструкции — всё, что не требует ожидания внешних событий.

Примеры синхронного кода:

const sum = 5 + 10;

console.log(sum);

function calculate(x) {

  return x * 2;

}

const result = calculate(20);

Весь этот код выполняется сразу, без задержек и очередей. JavaScript обрабатывает его от начала до конца, не переключаясь на другие задачи.

Асинхронные задачи

Асинхронные задачи — это операции, которые требуют времени или зависят от внешних событий, и потому их выполнение откладывается. Браузер или Node.js берут на себя обработку таких задач, не блокируя основной поток.

Типичные примеры асинхронных операций:

  • Таймеры: setTimeout, setInterval — ждут истечения времени.
  • События DOM: клики, нажатия клавиш, скролл — ждут действий пользователя.
  • Сетевые запросы: fetch, XMLHttpRequest — ждут ответа от сервера.
  • Промисы: .then(), .catch(), async/await — ждут разрешения промиса.

Когда вы вызываете асинхронную операцию, JavaScript не останавливается в ожидании её завершения. Вместо этого он регистрирует колбэк, продолжает выполнять следующий код, а когда асинхронная операция завершается, колбэк попадает в соответствующую очередь задач. Event Loop позже подберёт его и выполнит, когда придёт очередь.

Макрозадачи и микрозадачи: что это и чем отличаются

До 2015 года в JavaScript существовала только одна очередь задач — Task Queue, куда попадали все асинхронные операции: таймеры, события, сетевые запросы. Однако с выходом стандарта ES6 и появлением промисов в языке возникла необходимость в более гибкой системе приоритетов. Так появилась вторая очередь — Microtask Queue (очередь микрозадач), предназначенная для коротких, высокоприоритетных задач, которые должны выполниться сразу после текущего синхронного кода, не дожидаясь следующего оборота Event Loop.

Это разделение критически важно для понимания порядка выполнения асинхронного кода. Микрозадачи всегда обрабатываются раньше макрозадач, даже если последняя попала в очередь первой. Давайте разберёмся, что относится к каждому типу и почему это имеет значение.

Характеристика Макрозадачи (Macrotasks) Микрозадачи (Microtasks)
Что входит setTimeout, setInterval, setImmediate (Node.js), события DOM, I/O операции, requestAnimationFrame Промисы (.then, .catch, .finally), queueMicrotask, MutationObserver, process.nextTick (Node.js)
Приоритет Низкий — выполняются по одной за итерацию Event Loop Высокий — выполняются все подряд до опустошения очереди
Когда выполняются После обработки всех микрозадач Сразу после текущего синхронного кода и перед следующей макрозадачей
Использование Длительные операции, события пользователя, таймеры Быстрые операции, обработка результатов промисов

Микрозадачи (microtasks)

Микрозадачи — это задачи с наивысшим приоритетом в асинхронной очереди. Они создаются преимущественно при работе с промисами, а также через специальные API вроде queueMicrotask() или MutationObserver.

Ключевая особенность микрозадач: Event Loop обрабатывает их все без исключения сразу после завершения текущего синхронного кода, до того как взять хотя бы одну макрозадачу. Если во время выполнения микрозадачи создаётся новая, она тоже будет выполнена немедленно — очередь должна полностью опустеть, прежде чем Event Loop двинется дальше.

Примеры микрозадач:

Promise.resolve().then(() => {

  console.log('Микрозадача 1');

});

queueMicrotask(() => {

  console.log('Микрозадача 2');

});

Обе эти операции попадут в очередь микрозадач и выполнятся последовательно, сразу после синхронного кода.

Макрозадачи (macrotasks)

Это «обычные» асинхронные операции, которые попадают в основную очередь задач (Task Queue). К ним относятся таймеры, обработчики событий DOM, сетевые запросы и другие операции, требующие взаимодействия с окружением браузера.

Макрозадачи выполняются по одной за каждый цикл Event Loop. После выполнения такой задачи Event Loop сначала полностью обрабатывает все микрозадачи, и только потом переходит к следующей макрозадаче.

Примеры макрозадач:

setTimeout(() => {

  console.log('Таймер');

}, 0);

document.addEventListener('click', () => {

  console.log('Клик');

});

fetch('https://api.example.com/data')

  .then(response => console.log('Данные получены'));

Все эти операции — макрозадачи. Их колбэки попадут в Task Queue и будут выполнены по очереди, с учётом более высокого приоритета микрозадач.

В какой последовательности выполняются задачи

Понимание последовательности выполнения — это ключ к предсказуемому поведению асинхронного кода. Event Loop работает по строгому алгоритму:

  1. Выполнить весь синхронный код — это всегда первый шаг.
  2. Обработать очередь микрозадач полностью — все промисы, queueMicrotask и прочее.
  3. Взять одну макрозадачу из Task Queue и выполнить её.
  4. Снова обработать очередь микрозадач — если во время выполнения макрозадачи появились новые микро.
  5. Повторить цикл с шага 3.

Именно поэтому промисы почти всегда выполняются раньше таймеров, даже если setTimeout был вызван первым, а промис создан позже. Микрозадачи имеют абсолютный приоритет перед макрозадачами — это фундаментальное правило Event Loop, которое объясняет множество неочевидных на первый взгляд ситуаций в JavaScript.

Важно также понимать, что если микрозадачи порождают новые бесконечно, это может заблокировать выполнение макрозадач и привести к зависанию интерфейса — хотя на практике такие ситуации встречаются редко.

Разбор примеров: какой код выполняется сначала

Теория — это хорошо, но настоящее понимание Event Loop приходит через практику. Давайте разберём несколько типичных сценариев, которые часто встречаются в реальных проектах и на собеседованиях. Эти примеры помогут закрепить знания о приоритетах выполнения и увидеть, как микрозадачи и макрозадачи взаимодействуют друг с другом.

Пример 1: setTimeout с нулевой задержкой

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

 

console.log('Начало');

setTimeout(() => {

  console.log('Таймер');

}, 0);

console.log('Конец');

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

Выполняется console.log('Начало') -- выводится «Начало»
Встречается setTimeout -- его колбэк уходит в Web APIs, затем попадает в очередь макрозадач
Выполняется console.log('Конец') -- выводится «Конец»
Синхронный код завершён, Call Stack пуст
Event Loop забирает колбэк из очереди макрозадач -- выводится «Таймер»

Результат:

Начало

Конец

Таймер

Почему так происходит? Даже с нулевой задержкой setTimeout — это макрозадача, которая не может выполниться раньше, чем завершится весь синхронный код. Спецификация языка гарантирует минимальную задержку около 4 миллисекунд для таймеров, так что фактически «0» — это скорее «как можно скорее после освобождения стека», но не «прямо сейчас».

Пример 2: Promises и микрозадачи

Теперь усложним задачу и добавим промисы — посмотрим, как микрозадачи обгоняют макрозадачи в гонке за выполнение.

console.log('Старт');

setTimeout(() => {

  console.log('Таймер 1');

}, 0);

Promise.resolve()

  .then(() => {

    console.log('Промис 1');

  })

  .then(() => {

    console.log('Промис 2');

  });

console.log('Финиш');

Пошаговый разбор:

console.log('Старт') -- синхронный код, выполняется сразу → «Старт»
setTimeout регистрируется, его колбэк попадает в очередь макрозадач
Promise.resolve() создаёт resolved промис, первый .then() добавляется в очередь микрозадач
console.log('Финиш') -- синхронный код → «Финиш»
Синхронный код завершён, Event Loop обрабатывает микрозадачи:
Выполняется первый .then() → «Промис 1»
Это создаёт второй .then(), который тут же добавляется в очередь микрозадач
Выполняется второй .then() → «Промис 2»
Все микрозадачи обработаны, Event Loop переходит к макрозадачам:
Выполняется колбэк setTimeout → «Таймер 1»

Результат:

Старт

Финиш

Промис 1

Промис 2

Таймер 1

 

Обратите внимание: оба промиса выполнились до таймера, хотя setTimeout был зарегистрирован раньше. Это демонстрирует абсолютный приоритет микрозадач перед макрозадачами.

выполнение асинхронного кода.

Эта диаграмма наглядно демонстрирует, что микрозадачи («Промис 1» и «Промис 2») выполняются сразу после завершения всего синхронного кода («Старт» и «Финиш»), и только потом наступает очередь макрозадачи («Таймер»).

Пример 3: Смешанные случаи (таймеры + промисы)

А теперь самое интересное — разберём сложный случай, где макрозадачи и микрозадачи переплетаются, создавая друг друга. Именно такие примеры часто попадаются на собеседованиях и вызывают затруднения даже у опытных разработчиков.

console.log('Начало');

const promise1 = Promise.resolve().then(() => {

  console.log('Промис 1');

 

  setTimeout(() => {

    console.log('Таймер 2');

  }, 0);

});

const timer1 = setTimeout(() => {

  console.log('Таймер 1');

 

  Promise.resolve().then(() => {

    console.log('Промис 2');

  });

}, 0);

console.log('Конец');

Детальный разбор:

Выполняется console.log('Начало') → «Начало»
Создаётся промис, его .then() попадает в очередь микрозадач (содержимое: вывод «Промис 1» + создание «Таймер 2»)
Регистрируется setTimeout (Таймер 1), его колбэк уходит в очередь макрозадач
Выполняется console.log('Конец') → «Конец»
Обработка микрозадач:
Выполняется колбэк из promise1 → «Промис 1»
Внутри создаётся setTimeout (Таймер 2), его колбэк попадает в очередь макрозадач
Обработка макрозадач (первая итерация):
Выполняется Таймер 1 → «Таймер 1»
Внутри создаётся промис, его .then() попадает в очередь микрозадач
Снова обработка микрозадач:
Выполняется колбэк из промиса, созданного в Таймере 1 → «Промис 2»
Обработка макрозадач (вторая итерация):
Выполняется Таймер 2 → «Таймер 2»

Результат:

Начало

Конец

Промис 1

Таймер 1

Промис 2

Таймер 2

 

Этот пример наглядно показывает ключевое правило: после выполнения каждой макрозадачи Event Loop обязательно проверяет очередь микрозадач и обрабатывает её полностью. Именно поэтому «Промис 2», созданный внутри «Таймера 1», выполняется раньше «Таймера 2», хотя «Таймер 2» был зарегистрирован раньше.

Понимание таких сценариев критически важно для написания предсказуемого асинхронного кода и отладки сложных багов, связанных с порядком выполнения операций.

Event Loop в реальных сценариях браузера

До сих пор мы рассматривали Event Loop преимущественно в контексте таймеров и промисов, однако в реальных веб-приложениях его роль гораздо шире. Браузер постоянно обрабатывает множество событий: клики мыши, нажатия клавиш, скролл страницы, изменения DOM — и все они проходят через Event Loop. Понимание того, как цикл событий взаимодействует с пользовательским интерфейсом, помогает создавать более отзывчивые приложения и избегать типичных проблем с производительностью.

Рассмотрим практический пример с обработкой событий DOM:

document.getElementById('button').addEventListener('click', () => {

  console.log('Кнопка нажата');

 

  setTimeout(() => {

    console.log('Таймер после клика');

  }, 0);

 

  Promise.resolve().then(() => {

    console.log('Промис после клика');

  });

});

 

Когда пользователь нажимает кнопку, браузер фиксирует клик и ставит обработчик события в очередь макрозадач. Event Loop берёт этот обработчик и выполняет его.

Во время выполнения сначала выводится «Кнопка нажата», затем создаются таймер (это макрозадача) и промис (это микрозадача).

Когда обработчик клика заканчивается, Event Loop сразу выполняет все микрозадачи — поэтому появляется сообщение «Промис после клика». И только потом, на следующем проходе цикла, срабатывает таймер — выводится «Таймер после клика».

Важный нюанс: браузер обновляет отображение страницы (рендеринг) между макрозадачами, но не во время их выполнения. Это означает, что если ваш обработчик события выполняется слишком долго или создаёт бесконечную цепочку микрозадач, пользовательский интерфейс «зависнет» — браузер не сможет перерисовать экран до завершения текущей задачи.

Именно поэтому опытные разработчики разбивают тяжёлые вычисления на небольшие порции и распределяют их между несколькими макрозадачами через setTimeout или используют Web Workers для выноса вычислений в отдельный поток. Это позволяет Event Loop регулярно возвращать управление браузеру для обновления интерфейса, сохраняя отзывчивость приложения.

Ещё один практический аспект: порядок обработки событий. Если пользователь быстро кликает несколько раз, каждый клик создаёт отдельную макрозадачу, которая попадает в очередь. Event Loop обработает их последовательно, одну за другой, между ними выполняя все накопившиеся микрозадачи. Понимание этого механизма позволяет правильно реализовывать дебаунсинг, троттлинг и другие техники оптимизации обработки пользовательского ввода.

Async/await и Event Loop: что происходит «под капотом»

С появлением синтаксиса async/await в ES2017 работа с асинхронным кодом стала значительно удобнее и читабельнее. Однако важно понимать, что async/await — это не новый механизм асинхронности, а синтаксический сахар над промисами. Под капотом всё так же работает Event Loop с его очередями микрозадач и макрозадач.

Когда вы объявляете функцию как async, она автоматически возвращает промис. Каждый раз, когда внутри async-функции встречается ключевое слово await, выполнение функции приостанавливается до тех пор, пока промис не будет resolved, а продолжение её работы помещается в очередь микрозадач.

Рассмотрим пример:

console.log('Начало');

async function asyncFunction() {

  console.log('Async: старт');

  await Promise.resolve();

  console.log('Async: после await');

}

asyncFunction();

Promise.resolve().then(() => {

  console.log('Обычный промис');

});

console.log('Конец');

Что происходит пошагово:

Выполняется console.log('Начало') → «Начало»
Вызывается asyncFunction(), начинается её синхронное выполнение
Выполняется console.log('Async: старт') → «Async: старт»
Встречается await Promise.resolve() -- функция приостанавливается, а её продолжение (код после await) помещается в очередь микрозадач
Управление возвращается в основной поток, создаётся обычный промис, его .then() также попадает в очередь микрозадач
Выполняется console.log('Конец') → «Конец»
Синхронный код завершён, Event Loop обрабатывает микрозадачи в порядке их добавления:
Сначала продолжение asyncFunction → «Async: после await»
Затем колбэк обычного промиса → «Обычный промис»

Результат:

Начало

Async: старт

Конец

Async: после await

Обычный промис

 

Ключевой момент: каждый await создаёт точку приостановки, превращая оставшийся код функции в микрозадачу. Если в async-функции несколько await, каждый из них создаёт новую микрозадачу. Это означает, что между разными await могут выполниться другие микрозадачи из очереди.

Рассмотрим более сложный случай:

async function first() {

  console.log('First: 1');

  await Promise.resolve();

  console.log('First: 2');

  await Promise.resolve();

  console.log('First: 3');

}

async function second() {

  console.log('Second: 1');

  await Promise.resolve();

  console.log('Second: 2');

}

first();

second();

Здесь выполнение будет чередоваться: «First: 1», «Second: 1», затем обе функции приостановятся на первом await. Event Loop обработает микрозадачи в порядке их создания: сначала продолжение first() выведет «First: 2», потом продолжение second() выведет «Second: 2», затем first() снова приостановится на втором await, и в конце выведется «First: 3».

Понимание того, что await — это не магия, а просто более удобная запись работы с промисами через микрозадачи, помогает предсказывать поведение асинхронного кода и избегать распространённой ошибки: думать, что async/await делает код синхронным. Код остаётся асинхронным, просто записывается в более линейном, читабельном стиле.

Инструменты для изучения Event Loop

Теория и примеры кода — это основа, но визуализация процессов значительно ускоряет понимание сложных концепций. К счастью, существует несколько отличных инструментов, которые позволяют в реальном времени наблюдать за работой Event Loop, видеть, как задачи перемещаются между очередями, и отслеживать порядок их выполнения.

  • Loupe — один из самых популярных визуализаторов Event Loop, созданный Филипом Робертсом специально для его легендарного доклада «What the heck is the event loop anyway?». Этот инструмент доступен по адресу latentflip.com/loupe и позволяет пошагово выполнять JavaScript-код, наблюдая за состоянием Call Stack, Web APIs и Task Queue в каждый момент времени. Вы можете вводить свой код или использовать готовые примеры, что делает Loupe идеальным инструментом для экспериментов.
  • JS Visualizer — современная альтернатива с расширенной функциональностью. Этот инструмент (доступен на jsv9000.app) поддерживает не только макрозадачи, но и микрозадачи, что критически важно для понимания работы промисов и async/await. Интерфейс интуитивно понятен, визуализация чёткая, а возможность делиться ссылками на код делает его удобным для обучения и обсуждения сложных сценариев.
  • Chrome DevTools — встроенные инструменты разработчика Chrome предлагают вкладку Performance, где можно записать профиль выполнения вашего приложения и увидеть, как Event Loop обрабатывает задачи в реальном времени. Это особенно полезно для выявления узких мест производительности и понимания, какие операции блокируют основной поток. Вкладка Console также позволяет экспериментировать с асинхронным кодом непосредственно в браузере.

Использование этих инструментов в процессе обучения помогает перевести абстрактные концепции в наглядные визуальные образы, что значительно упрощает усвоение материала и формирует правильную ментальную модель работы Event Loop.

Частые ошибки и заблуждения разработчиков

Даже опытные JavaScript-разработчики иногда допускают ошибки в понимании Event Loop, что приводит к багам, которые трудно обнаружить и воспроизвести. Давайте разберём наиболее распространённые заблуждения и поймём, почему они возникают.

Заблуждение 1: «setTimeout(0) выполняется сразу»

Многие начинающие разработчики полагают, что если установить задержку таймера в 0 миллисекунд, колбэк выполнится немедленно. На практике же setTimeout(fn, 0) — это инструкция «выполни эту функцию как можно скорее, но только после завершения всего синхронного кода и обработки микрозадач». Минимальная реальная задержка составляет около 4 миллисекунд согласно спецификации. Это макрозадача, которая ждёт своей очереди, а не способ «выполнить прямо сейчас».

Заблуждение 2: «async/await делает код синхронным»

Синтаксис async/await выглядит как обычный последовательный код, что создаёт иллюзию синхронного выполнения. Однако это лишь синтаксический сахар над промисами. Когда функция встречает await, она не блокирует весь JavaScript — она приостанавливает только свою собственную работу, а управление возвращается в Event Loop. Остальной код продолжает выполняться, а продолжение async-функции попадает в очередь микрозадач. Код остаётся полностью асинхронным.

Заблуждение 3: «Promise всегда выполняется после setTimeout».

Это заблуждение противоположно реальности: промисы (микрозадачи) почти всегда выполняются раньше таймеров (макрозадач), независимо от того, в каком порядке они были созданы. Если вы видите, что промис выполнился после таймера, значит, либо сам промис был создан внутри макрозадачи, либо в коде происходит что-то неочевидное. Понимание приоритета очередей избавляет от этого заблуждения.

Осознание этих распространённых ошибок помогает писать более предсказуемый асинхронный код и быстрее находить причины неожиданного поведения программы.

Заключение

Мы разобрали механизм Event Loop от базовых концепций до сложных сценариев взаимодействия микрозадач и макрозадач. Давайте подведём итоги и выделим ключевые моменты, которые необходимо держать в голове при работе с асинхронным JavaScript:

  • Event loop в JS управляет порядком выполнения задач и делает однопоточный JavaScript неблокирующим. Он распределяет синхронный код и асинхронные колбэки так, чтобы интерфейс и серверные процессы оставались отзывчивыми.
  • Call stack выполняет код строго по принципу LIFO и не отдаёт управление, пока стек не освободится. Поэтому любые «длинные» синхронные операции напрямую блокируют выполнение остального.
  • Web APIs берут на себя таймеры, события и сетевые запросы, а затем возвращают колбэки в очереди. Это объясняет, почему setTimeout(0) не запускается мгновенно.
  • Макрозадачи и микрозадачи выполняются с разным приоритетом, и микрозадачи всегда обрабатываются раньше. Именно поэтому промисы часто «обгоняют» таймеры и события.
  • Async/await не делает код синхронным, а работает поверх промисов и очереди микрозадач. Понимание этого помогает предсказывать порядок вывода и избегать скрытых багов.
  • В браузере рендеринг происходит между макрозадачами, поэтому длинные обработчики и бесконечные микрозадачи могут «заморозить» UI. Для тяжёлых вычислений лучше дробление задач или Web Workers.

Если вы только начинаете осваивать профессию JavaScript-разработчика, рекомендуем обратить внимание на подборку курсов по JavaScript. В программах обычно есть теоретическая и практическая часть, чтобы закрепить понимание event loop, промисов и async/await на реальных задачах.

Читайте также
Категории курсов