Событийный цикл наглядно

Главная идея Node: один поток, но он никогда не стоит без дела.

Событийный цикл (event loop) — это механизм, который позволяет одному потоку Node обрабатывать множество операций: пока одна ждёт (запрос, файл), поток занимается другими.

Node — однопоточный. Как же он быстрый?

Ваш JavaScript в Node выполняется в одном потоке. Казалось бы, это медленно: один официант на весь зал. Но фокус в том, что официант не стоит у одного столика, пока на кухне готовят. Он принял заказ, отнёс на кухню и пошёл к следующему столику. Когда блюдо готово — кухня сообщает, и официант его подаёт.

Так же работает Node: запустил долгую операцию (чтение файла, запрос в сеть), не стал её ждать, а занялся другим кодом. Когда операция завершится, он вернётся к ней.

Стек вызовов и очередь

Упрощённо в Node есть две ключевые сущности:

  • Стек вызовов (call stack) — где прямо сейчас выполняется синхронный код. Он один.
  • Очередь задач (callback queue) — сюда складываются функции, которые «пора выполнить», когда их операция завершилась.

Event loop — это бесконечный цикл, который проверяет: «стек пуст? есть ли что-то в очереди? тогда возьму оттуда задачу и положу в стек».

Порядок выполнения удивляет новичков

Посмотрите на код. Что напечатается первым? Кажется, что сверху вниз — но setTimeout откладывает функцию в очередь, даже с задержкой 0:

console.log("1 — начало");

setTimeout(() => {
  console.log("3 — внутри setTimeout");
}, 0);

console.log("2 — конец");

Вывод:

1 — начало
2 — конец
3 — внутри setTimeout

Сначала выполнился весь синхронный код («1» и «2»), и только когда стек опустел, event loop забрал из очереди отложенную функцию («3»). Даже задержка 0 означает «после текущего синхронного кода», а не «прямо сейчас».

Почему это важно

Понимание event loop объясняет, почему Node не «зависает» на медленных операциях и почему асинхронный код выполняется не в том порядке, в каком написан. Это фундамент всего остального в этом разделе.

Микрозадачи: промисы идут вперёд таймеров

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

console.log("старт");

setTimeout(() => console.log("таймер"), 0);

Promise.resolve().then(() => console.log("промис"));

console.log("финиш");

Вывод:

старт
финиш
промис
таймер

Сначала синхронный код (старт, финиш), затем микрозадачи-промисы, и только потом таймеры. Это типичный вопрос на собеседованиях.

Итог

  • Node однопоточный, но не блокируется: долгие операции выполняются «в фоне».
  • Event loop забирает готовые задачи из очереди, когда стек вызовов пуст.
  • Синхронный код всегда выполняется раньше отложенного (даже setTimeout(.., 0)).
  • Промисы (микрозадачи) выполняются раньше таймеров.
Проверьте себя
1. Сколько потоков использует JavaScript-код в Node по умолчанию?
AОдин
BПо одному на каждый запрос
CСтолько, сколько ядер CPU
DНеограниченно
2. Что напечатается раньше: код после setTimeout(fn, 0) или сама fn?
AСначала fn
BСначала код после setTimeout, потом fn
CОдновременно
DЗависит от ОС
3. Что выполнится раньше при пустом стеке: колбэк промиса или setTimeout?
AsetTimeout
BКолбэк промиса (микрозадача)
CОни равноправны
DТот, что записан позже
Поддержать проект