JavaScript выполняет код в одном основном потоке. Это означает, что инструкции выполняются последовательно — одна за другой. Получил команду — выполнил. Но что делать интерпретатору, если он встречает код, который не может выполнить сразу? Например, обработчик события. Пока событие, допустим, клик на кнопку, не произошло, код внутри обработчика не выполнится. Такой код называют асинхронным. К асинхронным операциям относятся, например, таймеры (setTimeout), сетевые запросы или события интерфейса. Промисы (Promise) используются для обработки результатов таких операций. В такой ситуации на помощь интерпретатору JS приходит среда, в которой выполняется скрипт. Это может быть Node.js, мобильные среды или интерфейс, который предоставляет браузер — Web API (есть и другие). В отличие от JavaScript-движка, среда выполнения может использовать несколько потоков для обработки ввода-вывода, таймеров и сетевых операций.
Если сравнить выполнение скрипта с выступлением оркестра, то дирижёром, который отвечает, чтобы каждая функция «отыграла свою партию» в нужный момент, можно назвать Event Loop. Event Loop — это механизм среды выполнения, который управляет порядком выполнения задач. Он координирует работу JavaScript-кода, обработку событий и другие процессы браузера. Вопреки расхожему мнению, работа этого механизма не так сложна, как его часто описывают. В этой статье, проповедуя Фреймановскую истину — «Если не можешь объяснить что-то простыми словами, то ты не понимаешь этого» — автор попытается (для себя и для других) описать работу Event Loop в браузере. В среде Node.js концепция похожа, но вместо Web API используются другие механизмы ввода-вывода.
Поисковая выдача по запросу «как работает Event Loop в связке с JavaScript» содержит множество статей, в которых почти неизбежно появляются три «столпа»: стек вызовов, микротаски и макротаски — обычно со стрелочками между ними и объяснением в духе «сначала положили в одну кучку, потом в другую».
На практике такое объяснение часто только запутывает. Если заглянуть в спецификацию, то можно увидеть, что речь идёт всего о двух очередях: очереди задач (task queue) и очереди микрозадач (microtask queue). Call Stack при этом вообще не является очередью — это механизм выполнения кода, структура данных, в которой JavaScript хранит контексты выполняемых функций.
Выполнение исходного скрипта можно рассматривать как первую задачу (task) в Event Loop. Event Loop берёт задачу из очереди задач и помещает её в Call Stack. Разбираться со всеми синхронными контекстами интерпретатор будет по принципу LIFO (Last In, First Out), то есть последний добавленный контекст выполняется первым. Если код регистрирует асинхронную операцию, её выполнение передаётся среде выполнения. Когда операция завершится, соответствующий колбэк будет добавлен либо в очередь задач, а затем в Call Stack, либо в очередь микрозадач — в зависимости от типа операции.
После того как Call Stack становится пустым, Event Loop проверяет очередь микротасок (Microtask Queue) и выполняет все доступные микротаски. К микротаскам относятся Promise и MutationObserver. Важно: если в процессе выполнения микротасок создаются новые микротаски (например, в цепочке then промиса), они добавляются в ту же очередь и будут выполнены немедленно. Поскольку Event Loop выполняет все микротаски перед переходом к следующей задаче, большое количество микротасок может задерживать рендеринг страницы.
Затем Event Loop отслеживает, какую задачу взять следующей — в Web API поспела к выполнению ранее зарегистрированная таска? Давай её в Call Stack — будем выполнять. На случай, если в результате этого выполнения появились новые микротаски то Event Loop снова заглянет в очередь микротасок и выполнит всё, что накопилось. А затем снова перейдет к очереди задач и отправит в стек то, что уже готово к выполнению. Таким образом, Web API можно представить как систему, которая отслеживает асинхронные операции и сообщает Event Loop, когда их колбэки готовы к выполнению.
Вот как это выглядит по шагам:
1. Выполняется задача (Task) №1: Весь скрипт.
console.log('1'); // выполняется синхронно (часть текущей задачи) setTimeout(() => { // регистрируется в Web API console.log('макро'); // колбэк setTimeout станет отдельной следующей задачей №2 }, 0); Promise.resolve().then(() => { // Promise ставит свой колбэк в очередь микрозадач console.log('микро'); }); console.log('2'); // // выполняется синхронно (часть текущей задачи)
2. Скрипт закончился (первая задача выполнена), стек пуст.
3. Event Loop проверяет очередь микротасок (Microtask Queue) и выполняет всё, что там есть**.
// выполняется then() промиса console.log('микро');
4. Event Loop переходит к следующей задаче из очереди задач (Task Queue). Web API уже поместил туда колбэк от setTimeout, так как таймер истек.
// колбэк `setTimeout` (задача №2) console.log('макро');
Event Loop продолжает работать на протяжении всей жизни страницы, ожидая появления новых задач (например, событий пользователя, сетевых ответов или таймеров). Короткая шпаргалка:
task ↓ выполнение кода (Call Stack) ↓ microtasks ↓ render ↓ next task
На схеме task → Call Stack → microtasks → render → next task шаг render происходит не всегда, а лишь когда браузер решает, что пора обновить интерфейс (примерно 60 раз в секунду). Однако важно понимать порядок: даже если рендеринг не происходит, микротаски всегда выполняются до того, как начнется следующая макрозадача из очереди.
Ещё пример:
console.log('Start'); // Синхронно (в текущей task) setTimeout(() => { console.log('Timeout 1'); // Новая таска }, 0); Promise.resolve().then(() => { console.log('Promise 1'); // Микротаска }); setTimeout(() => { console.log('Timeout 2'); // Новая таска }, 0); Promise.resolve().then(() => { console.log('Promise 2'); // Микротаска }); console.log('End'); // Синхронно (в текущей таске) // 1. Выполняется синхронный код: Start, End. // 2. Очищается очередь микротасок: Promise 1, Promise 2. // 3. Берется первая таска из очереди: Timeout 1. // 5. Берется следующая таска: Timeout 2.
Вывод:
Start End Promise 1 Promise 2 Timeout 1 Timeout 2
И напоследок, хотя надо было с этого начать. Почему автор решил, что его объяснение тоже имеет право на существование? Ведь уже столько всего написано прекрасного про Event Loop.
Наверно, это немного тщеславия. Или то, чего когда-то не хватило автору — простого и понятного объяснения. Конечно, это упрощённый пересказ, но именно с него, как кажется, удобно начинать знакомство с этим механизмом.
Предполагается, что с таким подходом проще понять, как выполняется хитрый код со вложенными друг в друга колбэками, промисами и таймерами setTimeout. Для собеседований можно рисовать себе шпаргалку — в этом столбике у нас стек, тут — микрозадачи, а тут Web API держит в памяти задачи на будущее.
Для работы: понимание, что бесконечный while(true) блокирует стек и не дает запустить следующую микротаску, а бесконечный промис блокирует очередь микротасок, не давая запустить рендеринг, затем перейти к следующей задаче. Ситуация «я что-то написал и страница перестала реагировать» перестанет быть чем-то вроде злобных происков врагов.Таким образом, осознавая, хотя бы упрощённо, механизм работы Event Loop, вы приблизитесь к пониманию того, что такое утечки памяти и как их не допускать. Кстати, тоже интересная тема — но уже для новой статьи.
Ссылки для углубленного изучения:
Комментарии (9)

DmitryOlkhovoi
17.03.2026 10:29Сам по себе ивент луп довольно не простая штука, отличается в реализации у некоторых браузеров и ноды и прочих окружений. Вы описываете не то как он работает, что это, зачем оно, его природу и так далее. А просто разобрали вопросы на собесе, людей, которые также не всегда понимаю о чем спрашивают)))

TataSysueva Автор
17.03.2026 10:29Я хотела написать заметку о "не простой штуке" очень просто, насколько это возможно сделать, не погрешив против истины) Чтобы человек, который до сего дня ничего не слышал про евент луп, получил общее представление, а потом на этом основании строил "настоящее и сложное", ссылки я добавила. Не каждому дано перейти от "полного незнания", к чтению сложных статей и документаций. А если кто-то решит, что ему и этого хватит, лишь бы пройти собеседование, то пусть это останется на его совести)

Alexandroppolus
17.03.2026 10:29Предполагается, что с таким подходом проще понять, как выполняется хитрый код со вложенными друг в друга колбэками, промисами и таймерами
На простых примерах - да. Но иногда бывает с небольшим подвохом:
Пример
setTimeout(() => console.log(a - b), 0); const promise = new Promise((resolve) => { func(); resolve(); }); let a = 1; let b = 2; function func() { a++; } promise .then(() => console.log(a)) .catch(() => console.log(b--)) .catch(() => console.log(0)); Promise.resolve().then(() => console.log(3));

arseniikrilo
17.03.2026 10:29Просто его нужно прочувствовать, и тогда придёт осознание того, как он устроен в целом. Привычное понимание он ломает, но достаточно один раз понять, и понять как это применять. И тогда дополнительные вопросы перестанут возникать
Alexufo
Мне когда-то не хватало простой мысли в куче учебников по js, отчего такие приколы с setTimeout и с некоторыми другими функциями. А оказалось все просто - setTimeout не является функцией из js, а является функцией, которую добавляет окружение, т.е это реализация браузера Web APIs
js скачивается отдельно как бинарь, он готов интерпретировать ваш код, если там нет функций окружения. И изучать js необходимо именно с пониманием, что вот это язык, а вот этот язык расширяется окружением/средой. И с таким разделением сразу становится понятно, почему необходимы коллбеки промисы, код на выполнение выходит в другое место и результат вычислений необходимо забирать не простым return a+b.
DmitryOlkhovoi
JS не бинарь и не скачивается, и ничего не интерпретирует. Это интерпретируемый язык
Alexufo
Естественно я про интерпретатор говорил. Не найду сейчас репозиторий, который распространяет v8 в отдельным бинарем рядом с соответствующей версией хрома, но работает это вот так:
https://v8.dev/docs/d8
Чистый интерпретатор js без webapi
DmitryOlkhovoi
Это понятно, далее идут байндинги, и движки рендера
TataSysueva Автор
Интересное замечание, спасибо! Тоже заметила, что в JS и около есть темы, которые лучше понимаются, если их объясняют шире, чем принято...