Event Loop. Мифы и реальность

6fabb167383679ff25282465bc0cf159

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

В этой статье попробуем разобраться, где же, все таки, истина. Если вам кажется, что в статье имеется ошибка, неточность или чего-то не хватает, оставляйте свои комментарии, и мы попробуем совместно найти ответы на все возникающие сомнения и вопросы.

Итак, поехали!

Существует ли вообще понятие Event loop в JavaScript?

Понятие Event loop существует. Это факт. Но в спецификации ECMA-262 мы его не найдем. Дело в том, что Event loop не является частью языка JavaScript (ECMAScript) и, соответственно, не регулируется его спецификацией. Это понятие существует в рамках HOST-исполнителя, конкретная реализация движка JavaScript может использовать Event loop по своему усмотрения, в качестве API среды, в которой исполняется.

Что является официальным источником информации для Event loop?

Как мы выяснили выше, в спецификации ECMA-262 нет никаких упоминаний понятия Event loop, т.к. оно лежит вне зоны ответственности языка, но в зоне ответственности HOST-исполнителя, которая исполняет JavaScript код. Соответственно, искать информацию об Event loop следует в источниках, регулирующих, регламентирующих или документирующих данную среду-исполнителя. Таких сред, на сегодняшний день существует много. Условно, их можно разделить на две группы: браузерные и не браузерные.

Браузерные среды

Механизмы работы таких сред регламентирует организация WHATWG посредством спецификации HTML.

Конкретно Event loop посвящен раздел 8.1.7 Event loops. Об алгоритмах и стандартах Event loop в Web API поговорим чуть ниже. Пока упомяну только, что браузеры, как правило, оперируют API операционной системы, в которой исполняются, например, Chromium в MacOS полагается на NSRunLoop, а в Linux — на glib.

Исключением здесь, пожалуй, является Electron, который, в силу своей заявленной кросс-платформенности столкнулся со сложностями реализации Event loop для разных ОС и перешел на использование библиотеки libuv, по аналогии с Node.js (об этом дальше).

Не браузерные среды

Такие среды, как следует из названия, не занимаются реализацией стандарта HTML. И, поскольку нет никаких других международных стандартов и спецификаций (кроме ECMA-262), как должны такие среды работать, единственным официальным источникам информации можно считать только их собственную документацию.

Самой распространенной, на сегодняшний день, не браузерной средой является Node.js.

В документации Node.js (версия Node.js v21.6.1 на момент написания статьи) имеется раздел libuv even loop, в котором описывается единственная, доступная в Node API, функция napi_get_uv_event_loop, призванная получить ссылку на текущий Event loop.

Никакого другого описания Event loop эта документация, к сожалению, не дает. Но из неё очевидно, что для обеспечения работы оного, Node.js использует библиотеку libuv, которая разрабатывалась специально для Node.js, в основном, как раз для того, чтобы обеспечить реализацию Event loop в этой среде. Сейчас бибилотека используется и некоторыми другими проектами. Во внутренней документации библиотеки имеется раздел API uv_loop_t, дающий формальную спецификацию API в части Event loop. Так же, в документации имеется принципиальная схема работы Event loop, и гайд по работе с Event loop в рамках данной библиотеки

К другим не браузерным средам можно отнести Deno и Bun, которые так же полагаются на библиотеку libuv для работы с Event loop.

Мобильные среды, такие, как React Native,  NativeScript и Apache Cordova тоже являются не браузерными. Они полагаются на API соответствующей ОС, в которой исполняются. Например для Andriod это Android.os.Looper, а для iOS — RunLoop.

Но ведь без механизма Event loop, выполнение JavaScript-кода представить крайне трудно. Как спецификация ECMA-262 могла опустить такую важную часть?

Не смотря на то, что понятия Event loop в спецификации ECMA-262 нет, это не значит, что она никак не регламентирует процесс исполнение кода. Но регламентация эта не сосредоточена в одном конкретном понятии. Вообще, процессу исполнения JavaScript посвящен целый раздел 9 Executable Code and Execution Contexts. В частности, в п. 9.7 Agents, вводится понятие Agent, частью которого является структура Agent Record, которая, в свою очередь имеет поля, отвечающие за блокировку данного агента. Реализация агента остается на совести HOST-исполнителя, однако, имеются некоторые ограничения. В частности, п. 9.9 Forward Progress излагает основные требования к реализации агента:

  • Агент становится заблокированным, когда он синхронно ожидает какое-либо внешнее событие

  • Блокироваться может только агент, у которого поле ActiveRecord [[CanBlock]] имеет значение true

  • Разблокированным агентом считается тот, который не заблокирован

  • Каждый разблокированный агент с выделенным потоком исполнения в итоге должен продвинуться вперед

  • В наборе агентов, использующих один поток исполнения, вперед должен продвинуться только один

  • Агент не должен каким либо образом приводить к блокировке другого агента, кроме как через использование API, предназначенных специально для блокировок

Данных ограничений достаточно, чтобы, в сочетании с гарантиями раздела 29 Memory Model, все записи SEQ-CST в конечном итоге были observable для всех агентов.

Можно ли считать описание Event loop в спецификации HTML эталонным?

Так как официальная спецификация Event loop существует только в рамках стандарта HTML. Не браузерных же вариантов довольно много, все они сделаны на усмотрение разработчиков и у каждого есть свои особенности. Для каждого варианта потребуется отдельная статья (некоторые уже есть в сети). К тому же, многие реализации, так или иначе, опираются на спецификацию HTML, дабы не придумывать свои собственные велосипеды, что логично.

Считать ли спецификацию HTML эталонной в части Event loop — вопрос спорный. Однозначного ответа на этот счет нет, но, учитывая вышесказанное, для дальнейшего рассмотрения вопроса, с этого момента, будем оперировать именно этой спецификацией.

Обычно, при описании работы Event loop, говорят только о синхронных и асинхронных операциях JavaScript.

Как мы уже говорили ранее,  Event loop не лежит в поле языка JavaScript. Для JavaScript — это некий внешний механизм («сервис», если хотите), позволяющей организовать свою работу. При этом, сам Event loop не ограничивается только исполнением JS-кода. В зоне ответственности Event loop лежит множество процессов, таких как, операции ввода/вывода (события мыши и клавиатуры, чтение и запись файла, и пр.), координация событий, рендеринг, сетевые операции и много другое.

Является ли Event loop потокобезопасным?

Вопрос, на самом деле, интересный. Чуть выше мы говорили о разделе 9.9 Forward Progress спецификации ECMA-262, устанавливающим некоторые ограничения на реализацию агента. В этой части, прямого указания на потокобезопасность — нет. Наоборот, в разделе говориться о том, что при наличии нескольких агентов в одном потоке, продвинуться вперед должен только один из них. Такая модель явным образом говорит об отсутствии необходимости в потокобезопасности, так как, одновременно работать может только один агент.

В большинстве случаев так и есть. Например, библиотека libuv, используемая в Node.js, прямо заявляет о том, что их реализация не является потокобезопасной и использовать их Event loop следует однопоточно или самостоятельно позаботиться об организации работы в многопоточном режиме.

С браузерными же реализациям не все так однозначно.

Для начала, стоит уточнить, что в разделе 8.1.2 Agents and agent clusters спецификация HTML выделяет несколько видов агентов

  • Similar-origin window agent — может содержать несколько объектов Window, которые потенциально могут получить доступ друг к другу (напрямую или через document.domain)

  • Dedicated worker agent — содержит один DedicatedWorkerGlobalScope (область, имеющую явный MessagePort)

  • Shared worker agent — содержит один SharedWorkerGlobalScope (область, имеющая constructor origin,  constructor url и credentials)

  • Service worker agent — содержит один ServiceWorkerGlobalScope (область, имеющая ассоциированный service worker)

  • Worklet agent — содержит один WorkletGlobalScope (т.к. ворклеты могут импортировать ES модули, данная область имеет ассоциированную module map)

В зависимости от вида агента, спецификация выделяет три типа Event loop

  • window event loop — для similar-origin window agent

  • worker event loop — для dedicated worker agent,  shared worker agent и service worker agent

  • worklet event loop — для worklet agent

Из этих трех,  worker event loop и worklet event loop имеют флаг агента [[CanBlock]] равным true, что обязывает их следовать ограничениям 9.9 Forward Progress, и, соответствтенно, такие Event loop будут работать каждый в своем выделенном потоке.

Однако,  window event loop допускается использовать сразу несколько, одновременно в одном потоке (например, несколько табов браузера могут делить один поток, если того пожелает разработчик браузера).

Часто утверждается, что Event loop состоит из macrotasks и microtasks, так ли это?

Не совсем так. Понятия macrotask в спецификации не существует. Event loop состоит из task queue и microtask queue, при чем их механика принципиально различается.

Примечательно, что, несмотря на звание,  task queue очередью, на самом деле не является, это set. В то время, как microtask queue — действительно очередь. Дело в том, что на очередной итерации, в task queue может находиться множество задач в разных статусах. Алгоритм queue предполагает изъятие из очереди первого элемента (dequeue). Однако, в случае с task queue первый элемент не обязательно является runnable task в данный момент. Процесс, вместо dequeue должен найти первую задачу в статусе runnable task и извлечь её из набора, что нельзя считать реализацией алгоритма queue. Микротаски же, наоборот находятся в очереди и выводятся из неё в том порядке, в котором они в очередь попали. Детальнее разберем процесс ниже.

Что попадает в task queue?

В этом вопросе часто возникает когнитивный диссонанс. С одной стороны,  queue используется для отложенной обработки задач, т.е. асинхронного исполнения. Но что тогда происходит с синхронным кодом? Чтобы разобраться в этом, стоит немного выйти за рамки JavaScript (мы уже знаем, что Event loop лежит за его пределами) и осознать, что для браузера, JS-код сам по себе — всего лишь одна из множества сущностей, с которыми он работает. Распарсив файл скрипта или