Языки программирования

Event loop в JavaScript простыми словами: как работает асинхронность

JavaScript однопоточный — в любой момент времени выполняется только одна строка кода. И при этом он спокойно тянет сетевые запросы, таймеры и обработку кликов, не блокируя страницу. Кажется, что это противоречие, но разгадка не в магии, а в конкретном механизме — event loop. Разберёмся, как он устроен, из каких частей состоит и почему порядок выполнения асинхронного кода иногда удивляет даже опытных разработчиков.

Call stack: почему JavaScript может делать только одно дело за раз

У движка JavaScript есть call stack — стек вызовов функций. Когда вызывается функция, она кладётся на стек; когда функция возвращает значение, она снимается со стека. Пока стек не пуст, движок занят выполнением текущей цепочки вызовов и не может начать ничего нового. Именно это имеют в виду, когда говорят «JavaScript однопоточный»: есть один стек и один поток, который его обрабатывает.

Проблема в том, что многие операции — сетевой запрос, чтение файла, таймер — занимают время, и если бы движок ждал их прямо в стеке, страница бы «замерзала» на секунды. Решение: такие операции не выполняются в самом JavaScript-потоке. Они делегируются окружению — браузеру или Node.js, — а движок в это время свободен и может выполнять другой код.

Очередь задач и роль event loop

Когда делегированная операция завершается — таймер истёк, ответ сервера пришёл, — окружение не вклинивается в стек напрямую. Вместо этого callback-функция кладётся в очередь задач (task queue, также называемую macrotask queue). А event loop — это простой в своей идее механизм, который постоянно проверяет одно условие: пуст ли call stack. Как только он пуст, event loop берёт следующую задачу из очереди и кладёт её в стек на выполнение.

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

Микрозадачи: почему промисы обгоняют таймеры

Помимо очереди обычных задач, у event loop есть вторая, более приоритетная очередь — очередь микрозадач (microtask queue). Туда попадают колбэки промисов (.then, .catch, .finally), а также вызовы queueMicrotask. Правило простое и важное: после каждой отдельной задачи из основной очереди, прежде чем взять следующую, 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), затем — прежде чем event loop возьмёт из очереди задач таймер — полностью опустошается очередь микрозадач (3), и только после этого выполняется колбэк таймера (2).

Что из этого следует на практике

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

Event loop не ускоряет JavaScript и не делает его многопоточным — он просто определяет порядок, в котором единственный поток берётся за уже готовые задачи, пока сам ничем не занят.

Итог

Асинхронность в JavaScript держится на трёх частях: call stack выполняет код по одному вызову за раз, окружение берёт на себя долгие операции и возвращает результат через очереди, а event loop решает, что и когда забрать из этих очередей — сначала полностью опустошая микрозадачи, и только потом переходя к следующей обычной задаче. Как только эта картинка есть в голове, поведение асинхронного кода перестаёт быть набором заученных правил и превращается в предсказуемую механику, которую легко объяснить самому себе при отладке.

← Все статьи
Я люблю Алину Цой (Билялову)