Как работает event loop в javascript
С помощью механизма Event Loop (Цикл событий) становится возможным выполнять асинхронный код в JavaScript.
Event Loop — это специальный механизм на уровне движка js, который координирует работу трёх сущностей: Call Stack (стэк вызовов), Web API (API, предоставляемый браузером), Callback Queue (очередь колбэков).
Работают они следующим образом: движок js анализирует код. Когда он встречает вызов какой-то функции, он перемещает эту функцию в Call Stack. Если эта функция синхронная (например, console.log() ), то она сразу же исполняется, покидает стэк и на её место приходит следующая функция. Если же эта функция асинхронная, например, setTimeout() , обработчик событий, сетевой запрос и т.д., то на помощь приходит браузер со своим Web API (мы же помним, что JavaScript — это однопоточный язык, и сам работать в многопоточном режиме он не может). Event Loop перемещает колбэк асинхронной функции в Web API, а сама асинхронная функция уходит из стэка вызовов. То есть, пока колбэк асинхронной функции находится под управлением Web API, движок js продолжает выполнять другие операции!
Что же происходит с колбэком? В случае, например, setTimeout() , Web API ожидает истечения указанного времени, затем Event Loop перемещает этот колбэк в Callback Queue (очередь колбэков). Когда стэк вызовов освобождается, Event Loop перемещает в него наш колбэк из очереди колбэков, после чего колбэк наконец исполняется и покидает стэк вызовов.
Этот процесс повторяется до тех пор, пока весь js код не будет выполнен.
Здесь представлен наглядный пример работы Event Loop, очень советую ознакомиться!
01 октября 2022
Код JavaScript работает только в однопоточном режиме. Это означает, что в один и тот же момент может происходить только одно событие. С одной стороны это хорошо, так как такое ограничение значительно упрощает процесс программирования, здесь не возникает проблем параллелизма. Но, как правило, в большинстве браузеров в каждой из вкладок существует свой цикл событий. Среда управляет несколькими параллельными циклами. Общим знаменателем для всех сред является встроенный механизм, называемый Event Loop (Цикл событий) JavaScript, который обрабатывает выполнение нескольких фрагментов программы, вызывая каждый раз движок JS. Цикл событий — ключ к асинхронному программированию на JavaScript. Подробнее.
14 января 2023
Хотел бы еще добавить. В ES6 вместе с промисами появилось понятие очередь микротасков. Эта очередь используется промисами и обладает более высоким приоритетом, по сравнению с очередью макротасков (Например setTimeout или SetInterval). Это означает, что промисы будут выполняться раньше, чем вызовы из обычной очереди колбеков.
, 0); setTimeout(() => , 0); new Promise((resolve, reject) => ) .then(res => console.log(res)); new Promise((resolve, reject) => ) .then(res => console.log(res)); //Вывод Promise 1 resolved Promise 2 resolved setTimeout 1 setTimeout 2
The event loop
JavaScript has a runtime model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks. This model is quite different from models in other languages like C and Java.
Runtime concepts
The following sections explain a theoretical model. Modern JavaScript engines implement and heavily optimize the described semantics.
Visual representation
Stack
Function calls form a stack of frames.
function foo(b) const a = 10; return a + b + 11; > function bar(x) const y = 3; return foo(x * y); > const baz = bar(7); // assigns 42 to baz
Order of operations:
- When calling bar , a first frame is created containing references to bar ‘s arguments and local variables.
- When bar calls foo , a second frame is created and pushed on top of the first one, containing references to foo ‘s arguments and local variables.
- When foo returns, the top frame element is popped out of the stack (leaving only bar ‘s call frame).
- When bar returns, the stack is empty.
Note that the arguments and local variables may continue to exist, as they are stored outside the stack — so they can be accessed by any nested functions long after their outer function has returned.
Heap
Objects are allocated in a heap which is just a name to denote a large (mostly unstructured) region of memory.
Queue
A JavaScript runtime uses a message queue, which is a list of messages to be processed. Each message has an associated function that gets called to handle the message.
At some point during the event loop, the runtime starts handling the messages on the queue, starting with the oldest one. To do so, the message is removed from the queue and its corresponding function is called with the message as an input parameter. As always, calling a function creates a new stack frame for that function’s use.
The processing of functions continues until the stack is once again empty. Then, the event loop will process the next message in the queue (if there is one).
Event loop
The event loop got its name because of how it’s usually implemented, which usually resembles:
while (queue.waitForMessage()) queue.processNextMessage(); >
queue.waitForMessage() waits synchronously for a message to arrive (if one is not already available and waiting to be handled).
«Run-to-completion»
Each message is processed completely before any other message is processed.
This offers some nice properties when reasoning about your program, including the fact that whenever a function runs, it cannot be preempted and will run entirely before any other code runs (and can modify data the function manipulates). This differs from C, for instance, where if a function runs in a thread, it may be stopped at any point by the runtime system to run some other code in another thread.
A downside of this model is that if a message takes too long to complete, the web application is unable to process user interactions like click or scroll. The browser mitigates this with the «a script is taking too long to run» dialog. A good practice to follow is to make message processing short and if possible cut down one message into several messages.
Adding messages
In web browsers, messages are added anytime an event occurs and there is an event listener attached to it. If there is no listener, the event is lost. So a click on an element with a click event handler will add a message — likewise with any other event.
The first two arguments to the function setTimeout are a message to add to the queue and a time value (optional; defaults to 0 ). The time value represents the (minimum) delay after which the message will be pushed into the queue. If there is no other message in the queue, and the stack is empty, the message is processed right after the delay. However, if there are messages, the setTimeout message will have to wait for other messages to be processed. For this reason, the second argument indicates a minimum time — not a guaranteed time.
Here is an example that demonstrates this concept ( setTimeout does not run immediately after its timer expires):
const seconds = new Date().getTime() / 1000; setTimeout(() => // prints out "2", meaning that the callback is not called immediately after 500 milliseconds. console.log(`Ran after $new Date().getTime() / 1000 - seconds> seconds`); >, 500); while (true) if (new Date().getTime() / 1000 - seconds >= 2) console.log("Good, looped for 2 seconds"); break; > >
Zero delays
Zero delay doesn’t mean the call back will fire-off after zero milliseconds. Calling setTimeout with a delay of 0 (zero) milliseconds doesn’t execute the callback function after the given interval.
The execution depends on the number of waiting tasks in the queue. In the example below, the message «this is just a message» will be written to the console before the message in the callback gets processed, because the delay is the minimum time required for the runtime to process the request (not a guaranteed time).
The setTimeout needs to wait for all the code for queued messages to complete even though you specified a particular time limit for your setTimeout .
(() => console.log("this is the start"); setTimeout(() => console.log("Callback 1: this is a msg from call back"); >); // has a default time value of 0 console.log("this is just a message"); setTimeout(() => console.log("Callback 2: this is a msg from call back"); >, 0); console.log("this is the end"); >)(); // "this is the start" // "this is just a message" // "this is the end" // "Callback 1: this is a msg from call back" // "Callback 2: this is a msg from call back"
Several runtimes communicating together
A web worker or a cross-origin iframe has its own stack, heap, and message queue. Two distinct runtimes can only communicate through sending messages via the postMessage method. This method adds a message to the other runtime if the latter listens to message events.
Never blocking
A very interesting property of the event loop model is that JavaScript, unlike a lot of other languages, never blocks. Handling I/O is typically performed via events and callbacks, so when the application is waiting for an IndexedDB query to return or an XHR request to return, it can still process other things like user input.
Legacy exceptions exist like alert or synchronous XHR, but it is considered good practice to avoid them. Beware: exceptions to the exception do exist (but are usually implementation bugs, rather than anything else).
See also
- Event loops in the HTML standard
- The Node.js Event Loop, Timers, and process.nextTick() in the Node.js docs
Found a content problem with this page?
- Edit the page on GitHub.
- Report the content issue.
- View the source on GitHub.
This page was last modified on Sep 12, 2023 by MDN contributors.
Your blueprint for a better internet.
Что ты такое, Event Loop? Или как устроен цикл событий в браузере Chrome
Как думаете, что произойдет, если запустить в консоли браузера этот фрагмент кода?
function foo() < setTimeout(foo, 0); >foo();
function foo() < Promise.resolve().then(foo); >foo();
Если вы также, как и я, прочитали кучу статей про Event Loop, Main Thread, таски, микротаски и прочее, но затрудняетесь ответить на вопросы выше — эта статья для вас.
Итак, приступим. Код каждой HTML-страницы в браузере выполняется в Main Thread. Main Thread — это основной поток, где браузер выполняет JS, делает перерисовки, обрабатывает пользовательские действия и многое другое. По сути, это то место, где движок JS интегрирован в браузер.
Проще всего разобраться, глядя на схему:
Рисунок 1
Мы видим, что единственное место, через которое задачи могут попасть в Call Stack и выполниться — это Event Loop. Представьте, что вы оказались на его месте. И ваша работа успевать ‘разгребать’ задачи. Задачи могут быть двух типов:
- Личные — выполнение основного JavaScript-кода на сайте (далее будем считать, что он уже выполнился)
- Задачи от заказчиков — Render, Microtasks и Tasks
Конечно, первое, что приходит в голову — задать каждому заказчику приоритет, и выстроить их в очередь. Второе — определить, как именно будут обрабатываться задачи от каждого заказчика — по одной, все сразу или, может быть, пачками.
Взглянем на эту схему:

Рисунок 2
На основе этой схемы строится вся работа Event Loop.
После того как мы начали выполнять какой-либо script, в очередь Tasks ставится задача с выполнением этого скрипта. По мере выполнения этого кода, нам встречаются задачи от разных заказчиков, которые ставятся в соответствующие очереди. После того как завершается задача по выполнению скрипта (задача от Tasks), Event Loop идет к Microtasks (после задачи от Tasks Event Loop берет задачи от Microtasks). У него Event Loop берет задачи до тех пор, пока они не закончатся. Это значит, что если время их добавления равно времени их выполнения, то Event Loop будет бесконечно их разгребать.
Далее он идет к Render и выполняет задачи от него. Задачи от Render оптимизируются браузером и, если он посчитает, что в этом цикле не нужно ничего перерисовывать, то Event Loop просто пойдет дальше. Далее Event Loop снова берет задачи от Tasks и просит у него только одну, первую в очереди задачу, передает ее в CallStack и идет дальше по циклу.
Если у кого-то из заказчиков не оказалось задач, то Event Loop просто идет к следующему. И, наоборот, если у заказчика задачи занимают много времени, то остальные заказчики будут ждать своей очереди. А если задачи от какого-то заказчика оказались бесконечными, то Call Stack переполняется, и браузер начинает ругаться:

Рисунок 3
Теперь, когда мы поняли как работает Event Loop, пришло время разобраться, что будет после выполнения фрагментов кода в начале этой статьи.
function foo() < setTimeout(foo, 0); >foo();
Мы видим, что функция foo вызывает сама себя рекурсивно через setTimeout внутри, но при каждом вызове она создает задачу заказчика Tasks. Как мы помним, в цикле Event Loop при выполнении очереди задач от Tasks берет только 1 задачу в цикл. И далее происходит выполнение задач от Microtasks и Render. Поэтому этот фрагмент кода не заставит Event Loop страдать и вечно разгребать его задачи. Но будет подкидывать новую задачу для заказчика Tasks на каждом круге.
Давайте попробуем выполнить этот скрипт в браузере Google Chrome. Для этого я создал простой HTML-документ и подключил в нем script.js с этим фрагментом кода. После открытия документа заходим в инструменты разработчика, и открываем вкладку Perfomance и жмем там кнопку ‘start profiling and reload page’:

Рисунок 4
Видим, что задачи от Tasks выполняются по одной в цикл, примерно раз в 4ms.
Рассмотрим вторую задачку:
function foo() < Promise.resolve().then(foo); >foo();
Здесь мы видим тоже самое, что и в примере выше, но вызов foo добавляет задачи от Microtasks, а они выполняются все, пока не закончатся. А это значит, что пока Event Loop не закончит их, перейти к следующему заказчику он не сможет 🙁 И мы видим снова грустную картинку.
Взглянем на это в интрументах разработчкика:

Рисунок 5
Мы видим, что микротаски выполняются примерно раз в 0.1ms, и это в 40 раз быстрее, чем очередь Tasks. Все потому, что они выполняются все и сразу. В нашем примере очередь движется бесконечно. Для визуализации я уменьшил ее до 100 000 итераций.
Надеюсь, эта статья была вам полезной, и теперь вы понимаете, как работает Event Loop, и что ‘творится’ в примерах кода выше.
Всем пока 🙂 И до новых встреч. Если вам понравилось, ставьте лайки и подписывайтесь на мой канал 🙂
- event loop
- javascript
- requestanimationframe
- web api
- цикл событий
Как устроены цикл событий и стек вызовов в JavaScript

Цикл событий (event loop) — ключ к асинхронному программированию на JavaScript. Сам по себе язык однопоточный, но использование этого механизма позволяет создать дополнительные потоки, чтобы код работал быстрее. В этой статье разбираемся, как устроен стек вызовов и как они связаны с циклом событий.
Это адаптированный перевод выступления Роберта Филлипса, JavaScript-разработчика из британской компании Andea, на конференции JSConf. Повествование ведется от лица автора, скриншоты взяты отсюда.
Программировать на JavaScript я начал около 10 лет назад, и только недавно задумался, как этот язык программирования работает на самом деле. Долгое время однопоточность, движок V8, функции обратного вызова и циклы событий были для меня просто словами: я знал, как ими пользоваться, но не понимал, как они работают на глубоком уровне.
В процессе изучения я узнал, как работают стеки вызовов (call stack), циклы событий (event loop), очереди обратных вызовов (callback queue), интерфейсы API и другие сущности. В этой статье расскажу о некоторых своих открытиях.
Статья будет полезна как новичкам, так и опытным разработчикам. Первым она поможет понять, почему JavaScript настолько сильно отличается от других языков программирования и чем функция обратного вызова очень полезна на практике. Вторым — глубже разобраться в среде исполнения этого языка программирования.
Как устроена среда исполнения
На схеме ниже — упрощенное представление среды исполнения Runtime. В частности, движок V8 представлен как среда выполнения команд внутри браузера Google Chrome:

Распределение памяти происходит в heap, а stack frames хранятся в стеке вызовов (call stack).
Если мы попытаемся найти в исходном коде V8 функцию асинхронного программирования setTimeout , DOM или HTTP- запросы, то мы их там не найдем: за них, а также за AJAX-запросы, отвечает браузер, а не движок.
Как говорилось выше, JavaScript — это однопоточный (single threaded) язык программирования с одиночным стеком вызовов. Это означает, что в один момент JavaScript может выполнять только одну операцию (обработать только один кусок кода).
Рассмотрим пример с несколькими функциями: одна перемножает числа, другая возводит получившиеся число в квадрат, а третья — выводит результат на экран.

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

Стек вызовов создается, когда внутри функции (или метода) существуют другие функции. В реальных приложениях таких уровней могут быть сотни. В нашем примере это возведение в квадрат — функция внутри функции выведения на экран результата. Стек вызовов формируется каждый раз, когда вы запускаете код на стороне браузера.
Рассмотрим пример кода ниже: baz — функция, которая после запуска в Chrome вызывает bar , затем foo и возвращает ошибку.

В качестве результата выполнения функция выводит на экран трассировку стека (stack trace) — состояние стека в тот момент, когда произошла ошибка. Это пример того, что называется «неполадками в стеке» (blowing the stack).

Посмотрим где именно возникла ошибка. В стеке мы видим очередь вложенных функций foo, bar, baz и анонимную функцию, которая в данном случае главная.

В этом случае есть главная функция, которая вызывает foo , которая, в свою очередь вызывает foo , та снова вызывает foo , снова foo и так далее. В какой-то момент Chrome говорит нам: «Возможно, рекурсивно вызывать foo 16000 раз не стоит? Я просто отключу эти функции, чтобы вы могли изучить код программы и найти ошибку».

Перейдем к следующему вопросу: почему программа выполняется медленно? На скорость, например, влияет выполнение условного цикла от одного до миллиарда, а также выполнение запросов по сети. К медленным можно отнести и запросы на загрузку картинок. Большое количество медленных запросов тормозит стек.
Рассмотрим пример: у нас есть код с функцией getSync() — это функция jQuery с AJAX-запросом.

Представим, что эти запросы — синхронные. При выполнении кода в текущем виде сначала вызывается функция getSync , а затем — сетевой запрос (network request). Скорость выполнения последнего зависит от компьютера и может быть довольно низкой.

После того, как сетевой запрос прошел, выполняются все три функции. Теперь мы можем очистить стек.
В однопоточном языке программирования (в отличие, например, от Ruby с его threads) для выполнения всех функций нужно ждать выполнения сетевого запроса. Здесь возникает проблема: код выполняется в браузере, который блокирует его на время выполнения этого синхронного запроса: он не может ни отрисовывать изображения, ни выполнять другой код.
Если мы хотим сделать хороший пользовательский интерфейс, блокировки стека допускать нельзя. Самое очевидное решение — использовать асинхронные обратные вызовы (asynchronous callbacks). В этом случае браузер не блокирует функции, а при выполнении кода задается коллбэк, который выполняется позднее.
Рассмотрим на простом примере, как работают асинхронные вызовы:

При запуске функция setTimeout ставит console.log в очередь на пять секунд. В это время выводится JSConfEU, а в конце на экран выводится there .
При запуске кода мы видим, что функции выполняются одновременно. Среда исполнения JS Runtime в один момент времени может выполнить только одну функцию: например, не может выполнить AJAX-запрос во время выполнения другого кода. В данном случае несколько функций выполняются одновременно, поскольку браузер — это не только среда исполнения. В нем есть еще API, это позволяет совершать несколько действий одновременно.
Функция setTimeout в данном случае выполняется через web-API, который предоставляет браузер. В исходном коде движка V8 этой функции нет. Web-API не изменяет код, а только помещает отдельные команды на стек. Кроме того, любой web-API помещает коллбэк в очередь задач после своего выполнения.
Цикл событий
Теперь мы знаем достаточно, чтобы перейти к главной теме этой статьи — рассказу о том, как работают циклы событий (event loop). Главная задача циклов событий — следить за стеком и очередью задач. Если стек пуст, цикл берет первый элемент из очереди, помещает его в стек и выполняет.

На картинке выше видно, что если стек пуст, то цикл берет первый элемент из очереди задач и помещает его в стек. В очереди задач при этом находится коллбэк.
Первая сложность, с которой мы сталкиваемся — установка нулевого времени в функции setTimeout . Она ведет себя странно: переданный колбек не сразу выполняется, а попадает в очередь задач. Причина в механизме работы цикла событий: он ждет очистки стека и когда это происходит, помещает колбек в стек. Затем выполняются другие функции.
Установка нулевого времени в функции setTimeout приводит к тому, что она откладывает исполнение колбека и переносит его в конец стека.
Все web-API работают похожим образом. Например, AJAX-запрос на URL-адрес с обратным вызовом будет выполняться точно также.
XHR-запрос будет выполняться параллельно — он может быть не выполнен никогда, но стек при этом будет продолжать работать.

Рассмотрим пример: перед выступлением я написал инструмент, который визуализирует среду выполнения JavaScript Runtime в реальном времени. Судя по коду, программа должна вывести в лог сообщения при срабатывании клика и колбека в setTimeout .

Если мы добавим DOM API и задержку по времени, код продолжит работать: коллбэк просто встанет в очередь.
Если я нажму на кнопку «Click Me!», запустится web-API, который добавит в очередь коллбэк и запустит его. Если нажать на кнопку десять раз, вызов встает в очередь и обрабатывается.

Рассмотрим еще один пример с асинхронным API: в нем мы вызываем функцию setTimeout четыре раза, с односекундной задержкой, после чего выполняем команду console.log, которая выводит на экран hi .
К тому времени, как все обратные вызовы окажутся в очереди, четвертый запрос с секундной задержкой будет находиться в состоянии ожидания. Этот пример демонстрирует минимальное время на исполнение функций в очереди — setTimeout не выполняет код сразу же, а сделает это в порядке очереди. Точно также как функция setTimeout с нулевой задержкой не вызывает колбек сразу же.
На этом примере хорошо видна разница между двумя типами обратных вызовов: они могут быть либо функцией, которую вызывает другая функция, либо асинхронным вызовом.
Другой пример: метод forEach берёт переданную функцию-колбек и запускает её внутри стека, не асинхронно.

Метод forEach здесь выполняется медленно — после очереди из обратных вызовов мы можем выполнить код и вывести результат на экран через console.log .
Рендеринг
Определенные действия в JS накладывают на рендеринг в браузере ограничения. В идеальной ситуации браузер перерисовывает экран раз в 16,6 миллисекунд и воспроизводит результат со скоростью 60 кадров в секунду.
Однако пока в стеке есть код, браузер не может проводить рендеринг. Если рендер можно назвать вызовом, то в нем почти всегда есть коллбэк, которому для начала работы нужно дождаться очистки стека. Разница в том, что приоритет рендера выше: каждые 16 миллисекунд он ставит отрисовку в очередь.
Что означает блокировка рендера? Пока выполняется код, невозможно выделить текст на экране, кликать на кнопки и видеть отклик.

Код на картинке выше организован так, что рендер выполняется асинхронно — в коде оставлено место для того, чтобы он выполнял отрисовку между каждым элементом.
Все, о чем мы говорили выше — имитация того, как работает рендер. Но это хороший способ показать, что имеют в виду программисты, которые предостерегают от блокировки цикла событий.
Предостережения звучат так: «не используйте медленный код, иначе браузер не сможет рендерить красивые и органичные элементы интерфейса». Поэтому когда вы решаете задачу обработки изображений или анимирования большого количества изображений, но при этом не уделили должного внимания очереди, код будет работать медленно. В этой статье мы убедились, что это чистая правда.
- асинхронность
- асинхронное программирование
- javascript
- event loop
- callstack
- разработка
- однопоточный веб
- очередь задач
- разработка по
- Блог компании Хекслет
- Веб-разработка
- JavaScript
- Программирование
