Event Loop. Мифы и реальность
В сети есть довольно большое количество публикаций про 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-код сам по себе - всего лишь одна из множества сущностей, с которыми он работает. Распарсив файл скрипта или <script>
тэг, браузер получает токенизированный результат, обработка которого, сама по себе является отдельной задачей, для чего генерируется задача типа global task в Event loop. Таким образом, фактически, синхронный код с самого начала своего исполнения уже находится внутри Event loop в виде задачи.
Далее, по мере выполнения скрипта, генерируются новые задачи и помещаются в тот же Event loop.
Что еще попадает в task queue
- События - отправка события часто попадает в task queue, но не всегда. Есть много событий, отправка которых осуществляется в других задачах. Например, события
MouseEvent
иKeyboardEvent
могут быть объеденены в одну задачу,source
которой ассоциирован с user interaction task source - Парсинг - HTML parser токенизирует один или несколько байт, потом обрабатывает полученный токенизированный результат
- Колбэки - как правило, попадают в task queue, это же относится и к классическому примеру
setTimeout(() => {})
, в данном случае вsetTimeout
передается колбэк, который встанет отдельной задачей в task queue - Запрос ресурсов - в случае не блокируемой загрузки ресурса (например, изображения), ставится отдельная задача в task queue
- Манипуляции с DOM - некоторые элементы генерируют задачу в task queue в ответ на манипуляции с DOM. Как пример, вставка нового элемента в DOM спровоцирует постановку задачи на перерисовку родительского элемента.
Зачем нужна microtask queue, если есть task queue и что в неё попадает?
В случае с task queue, работа по определению и добавлению задач в очередь лежит на плечах агента. Работа же c microtask queue - это, своего рода опция, доступная задаче во время исполнения через Web API, что позволяет данной конкретной задаче реализовать какие-либо свои собственные асинхронные потребности. Технически, каждая задача, будь то исполнение JS-кода, токенизация HTML текста, события ввода-вывода или что-либо еще, может использовать microtask queue для реализации своих собственных целей.
Если говорить конкретно о JavaScript, язык, в своей реализации, предполагает наличие собственных асинхронных операций, не покрытых спецификацией HTML. Операции эти, как вы уже догадались - Promise. Если быть более конкретным, раздел 27.2.1.3.2 Promise Resolve Functions спецификации описывает процесс разрешения промиса. Шаг 15 предполагает выполнение HostEnqueuePromiseJob, реализация которой остается на HOST-исполнителя, но соответствовать некоторым требованиям, например, Jobs должны запускать в том же порядке, в котором были вызваны, запланировавшие их HostEnqueuePromiseJobs.
Как мы уже говорили, формально, вопрос реализовации механизма HostEnqueuePromiseJob, полностью лежит на совести HOST-исполнителя. Однако, использование тут microtask queue звучит крайне логично. Но, для большей достоверности, обратимся к исходному коду одно из самы широко используемых JS-движков - V8 (на момент написания статьи, последняя версия движка 12.3.55)
// https://tc39.es/ecma262/#sec-promise-resolve-functions // static MaybeHandle<Object> JSPromise::Resolve(Handle<JSPromise> promise, Handle<Object> resolution) { //... //... //... // 13. Let job be NewPromiseResolveThenableJob(promise, resolution, // thenAction). Handle<NativeContext> then_context; if (!JSReceiver::GetContextForMicrotask(Handle<JSReceiver>::cast(then_action)) .ToHandle(&then_context)) { then_context = isolate->native_context(); } Handle<PromiseResolveThenableJobTask> task = isolate->factory()->NewPromiseResolveThenableJobTask( promise, Handle<JSReceiver>::cast(resolution), Handle<JSReceiver>::cast(then_action), then_context); if (isolate->debug()->is_active() && IsJSPromise(*resolution)) { // Mark the dependency of the new {promise} on the {resolution}. Object::SetProperty(isolate, resolution, isolate->factory()->promise_handled_by_symbol(), promise) .Check(); } MicrotaskQueue* microtask_queue = then_context->microtask_queue(); if (microtask_queue) microtask_queue->EnqueueMicrotask(*task); // 15. Return undefined. return isolate->factory()->undefined_value(); }
В реализации V8 мы видим, что для выполнения HostEnqueuePromiseJob, движок ставит соответствующую microtask в очередь microtask queue, подтверждая, тем самым, наше предположение.
Как, все таки, работает алгоритм Event loop? Можно ли где-нибудь посмотреть пример реализации?
Как уже упоминалось выше, существует множество вариантов реализации алгоритма. Браузерные среды руководствуются спецификацией HTML, а для реализации механизма, как правило, полагаются на API той ОС, в которой исполняются. Искать примеры реализации, в этом случае, стоит в исходных кодах каждого конкретного браузерного движка, а так же, в соответствующих библиотеках и ОС API. В платформах, использующих сторонние библиотеки, такие как libuv, пример реализации стоит искать в самой этой библиотеке (libuv имеет открытый исходный код). Однако, надо понимать, что каждая реализация самостоятельно и может сильно отличаться от других.
В качестве иллюстрации и дабы не полагаться на какую-то конкретную реализацию, попробуем изобразить свою псевдо-версию механизма. Раз уж мы говорим в контексте JavaScript, для простоты понимания и восприятия, реализовывать будем на языке Typescript.
В следующем листинге приведены необходимые, для работы Event loop, интерфейсы. Эти интерфейсы оставлены исключительно в демонстрационных целях в соответствтии со спецификацией HTML и никак не отражают внутренние структуры какого-либо реального HOST-исполнителя. Сам алгоритм Event loop будет приведен далее.
/** * A browsing context is a programmatic representation of a series of documents, multiple of which can live within a * single navigable. Each browsing context has a corresponding WindowProxy object, as well as the following: * * - An `opener browsing context`, a browsing context or null, initially null. * - An `opener origin` at creation, an origin or null, initially null. * - An `is popup` boolean, initially false. * - An `is auxiliary` boolean, initially false. * - An `initial UR`L, a URL or null, initially null. * - A `virtual browsing context group ID` integer, initially 0. This is used by cross-origin opener policy reporting, * to keep track of the browsing context group switches that would have happened if the report-only policy had been * enforced. * * A browsing context's active window is its WindowProxy object's [[Window]] internal slot value. A browsing context's * active document is its active window's associated Document. * * A browsing context's top-level traversable is its active document's node navigable's top-level traversable. * * A browsing context whose is auxiliary is true is known as an auxiliary browsing context. Auxiliary browsing contexts * are always top-level browsing contexts. * * Note: For a demonstration purposes and for simplicity the BrowserContext is reflecting the Window interface which is * not fully correct, as the might be different implementations of the BrowserContext. */ interface BrowsingContext extends Window {} /** * A navigation request is a request whose destination is "document", "embed", "frame", "iframe", or "object" * * Note: For a demonstration purposes and for simplicity the NavigationRequest is reflecting the Window interface * which is not correct as the NavigationRequest is a different structure mostly use for * `Handle Fetch` (https://w3c.github.io/ServiceWorker/#handle-fetch) */ interface NavigationRequest extends Window {} interface Environment { id: string; creationURL: URL; topLevelCreationURL: URL; topLevelOrigin: string | null; targetBrowsingContext: BrowsingContext | NavigationRequest | null; activeServiceWorker: ServiceWorker | null; executionReady: boolean; } interface EnvironmentSettingsObjects extends Environment { realmExecutionContext: ExecutionContext; moduleMap: ModuleMap; apiBaseURL: URL; origin: string; policyContainer: PolicyContainer; crossOriginIsolatedCapability: boolean; timeOrigin: number; } interface Task { // A series of steps specifying the work to be done by the task. // will be defined in a certain Task implementation steps: Steps; // One of the task sources, used to group and serialize related tasks. // // Per its source field, each task is defined as coming from a specific task source. For each event loop, every // task source must be associated with a specific task queue. // // Essentially, task sources are used within standards to separate logically-different types of tasks, // which a user agent might wish to distinguish between. Task queues are used by user agents to coalesce task sources // within a given event loop. source: unknown; // A Document associated with the task, or null for tasks that are not in a window event loop. // A task is runnable if its document is either null or fully active. document: Document | null; // A set of environment settings objects used for tracking script evaluation during the task. environmentSettingsObject: Set<EnvironmentSettingsObjects>; } interface GlobalTask extends Task { steps: Steps; // redefine/implement steps for this particular task type } interface EventLoop { taskQueue: Set<Task>; // Each event loop has a microtask queue, which is a queue of microtasks, initially empty. A microtask is a colloquial // way of referring to a task that was created via the queue a microtask algorithm. // // For microtaskQueue is used just to illustrate that the specification supposes it to be a logical queue, rather // than a set of tasks. From technical perspective, a real implementation might use a `Set` like for taskQueue, or // any other structure at the discretion of the agent's developer. microtaskQueue: Array<Task>; // Each event loop has a currently running task, which is either a task or null. Initially, this is null. It is used // to handle reentrancy. currentRunningTask: Task | null; // Each event loop has a performing a microtask checkpoint boolean, which is initially false. It is used to prevent // reentrant invocation of the perform a microtask checkpoint algorithm. performingAMicrotaskCheckpoint: boolean; } interface WindowEventLoop extends EventLoop { // Each window event loop has a DOMHighResTimeStamp last render opportunity time, initially set to zero. lastRenderOpportunityTime: number; // Each window event loop has a DOMHighResTimeStamp last idle period start time, initially set to zero. lastIdlePeriodStartTime: number; } /** Just for demonstration purposes. Such a helper not necessarily should be presented in the real implementation */ function isWindowEventLoop(eventLoop: EventLoop): eventLoop is WindowEventLoop { return 'lastRenderOpportunityTime' in eventLoop && 'lastIdlePeriodStartTime' in eventLoop; }
Необходимые интерфейсы мы описали (по крайней мере ту часть, которая нам нужна для понимания алгоритмы ниже). Теперь, собственно, сам алгоритм Event loop. Опять же, стоит уточнить, что алгоритм является иллюстрацией раздела 8.1.7.3 Processing model спецификации и никак не отражает реальные имплементации на HOST-исполнителе.
/** * Processing the event loop according to the `8.1.7.3 Processing model` * https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model */ function runEventLoop(eventLoop: EventLoop) { // 1. Let oldestTask and taskStartTime be null. let oldestTask: Task | null = null; let taskStartTime: number | null = null; while (true) { // 2. check if the taskQueue has a runnable task and if there is one // 2.1. Let taskQueue be one such task queue, chosen in an implementation-defined manner. // 2.2. ... will be done below // 2.3. Set oldestTask to the first runnable task in taskQueue, and remove it from taskQueue. oldestTask = getFirstRunnableTaskFromQueueAndRemove(eventLoop.taskQueue); if (oldestTask !== null) { // 2.2. Set taskStartTime to the unsafe shared current time. taskStartTime = Date.now(); // 2.4. Set the event loop's currently running task to oldestTask. eventLoop.currentRunningTask = oldestTask; // 2.5. Perform oldestTask's steps. performTaskSteps(oldestTask.steps); // 2.6. Set the event loop's currently running task back to null. eventLoop.currentRunningTask = null; // 2.7. Perform a microtask checkpoint. performMicrotaskCheckpoint(eventLoop); } // 3. Let hasARenderingOpportunity be false. let hasARenderingOpportunity = false; // 4. Let `now` be the unsafe shared current time. let now = Date.now(); // 5. If oldestTask is not null, then: if (oldestTask !== null) { // 5.1. Let top-level browsing contexts be an empty set. const topLevelBrowsingContexts = new Set(); // 5.2. For each environment settings object settings of oldestTask's script evaluation // environment settings object set: oldestTask.environmentSettingsObject.forEach((settingsObject) => { // 5.2.1. Let `global` be settings's global object. const global = settingsObject.targetBrowsingContext; // 5.2.2. If `global` is not a Window object, then continue. if (!(global instanceof Window)) { return; } // 5.2.3. If global's browsing context is null, then continue. if (!global.document) { return; } // 5.2.4. Let tlbc be global's browsing context's top-level browsing context. const tlbc = global.document; // 5.2.5. If tlbc is not null, then append it to top-level browsing contexts. if (tlbc !== null) { topLevelBrowsingContexts.add(tlbc) } }); // 5.3. Report long tasks, passing in taskStartTime, now (the end time of the task), top-level browsing contexts, // and oldestTask. // https://w3c.github.io/longtasks/#report-long-tasks // ... } // 6. if this is a window event loop, then: Update the rendering if (isWindowEventLoop(eventLoop)) { updateRendering(eventLoop); } // 7. If all of the following are true: // - this is a window event loop; // - there is no task in this event loop's task queues whose document is fully active; // - this event loop's microtask queue is empty; and // - hasARenderingOpportunity is false, // then: // ...run computeDeadline and hasPendingRenders steps for WindowEventLoop // 8. If this is a WorkerEventLoop, then: // ...run animation frame callbacks and update the rendering of that dedicated worker }
Часть операций, согласно спецификации, может быть вынесена в отдельные функции и алгоритмы, такие как, например, шаг 2.7 Perform a microtask checkpoint , который мы вынесли в отдельную функцию performMicrotaskCheckpoint
.
/** Finds and returns the first runnable task in the queue. The found Task will be removed from the queue */ function getFirstRunnableTaskFromQueueAndRemove(taskQueue: Set<Task>): Task | null { //... return null; } /** Performs Task steps */ function performTaskSteps(steps: Steps) { //... } /** * Performs a microtask checkpoint * https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint */ function performMicrotaskCheckpoint(eventLoop: EventLoop) { // 1. If the event loop's performing a microtask checkpoint is true, then return. if (eventLoop.performingAMicrotaskCheckpoint) { return; } // 2. Set the event loop's performing a microtask checkpoint to true. eventLoop.performingAMicrotaskCheckpoint = true; // 3. While the event loop's microtask queue is not empty: while (eventLoop.microtaskQueue.length > 0) { // 3.1. Let oldestMicrotask be the result of dequeuing from the event loop's microtask queue. const oldestMicrotask = eventLoop.microtaskQueue.shift(); // 3.2. Set the event loop's currently running task to oldestMicrotask. eventLoop.currentRunningTask = oldestMicrotask; // 3.3. Run oldestMicrotask. performTaskSteps(oldestMicrotask.steps); // 3.4. Set the event loop's currently running task back to null. eventLoop.currentRunningTask = null; } // 4. For each environment settings object whose responsible event loop is this event loop, notify about rejected // promises on that environment settings object. // ... // 5. Cleanup Indexed Database transactions. // ... // 6. Perform ClearKeptObjects(). // ... // 7. Set the event loop's performing a microtask checkpoint to false. eventLoop.performingAMicrotaskCheckpoint = false; } /** * Runs `Update the rendering` steps for WindowEventLoop * https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering */ function updateRendering(eventLoop: WindowEventLoop) { // ... reveal that Document // ... flush autofocus candidates for that Document // ... run the resize steps for that Document // ... run the scroll steps for that Document // ... evaluate media queries and report changes for that Document // ... update animations and send events for that Document // ... run the fullscreen steps for that Document // ... run the animation frame callbacks for that Document // ... if the focused area of that Document is not a focusable area, then run the focusing steps for that // Document's viewport // ... perform pending transition operations for that Document // ... run the update intersection observations steps for that Document // ... invoke the mark paint timing algorithm // ... update the rendering or user interface of that Document and its node navigable // ... run process top layer removals given Document. }
Резюмируя выше сказанного
Само понятие Event loop, существует, но находится вне зоны ответственности спецификации ECMA-262.
Официальным источником информации для Event loop можно считать спецификацию HTML, в случае с браузерными средами, либо официальную документацию библиотек или самих HOST-исполнителей, в случае с не браузерными средами.
В спецификации ECMA-262 есть косвенные отсылки к процессам, связанным с Event loop, но реализацию этих процессов спецификация оставляет на совести HOST-исполнителя.
Event loop не завязан исключительно на обслуживание JavaScript кода. На самом деле, JavaScript - всего лишь один из типов задач, которые могут попасть в Event loop. По мимо JavaScript, браузер может помещать сюда задачи, например, на токенизацию полученного HTML текста, на обработку операций ввода/вывода, на отрисовку элементов на экране, и еще много чего другого.
Event loop, согласно спецификации HTML, не обязан быть потокобезопасным, но может таковым являться. В случае исполнения нескольких агентов со своим Event loop в одном потоке, они должны самостоятельно организовать алгоритм взаимодействия друг с другом, так, чтобы в одну единицу времени разблокированным агентом был только один из них, остальные должны находиться в состоянии блокировки.
Event loop состоит из task queue и microtask queue. В task queue попадают задачи, поставленные HOST-исполнителем, microtask queue - это опциональная возможность для задачи из task queue воспользоваться Web API исполнителя с целью выполнения каких-либо своих специфических асинхронных подзадач.
EN - https://t.me/frontend_almanac
RU - https://t.me/frontend_almanac_ru
English version: https://blog.frontend-almanac.com/event-loop-myths-and-reality