Событийный цикл наглядно
Главная идея 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)). - Промисы (микрозадачи) выполняются раньше таймеров.