Event Loop:事件循环的神话与现实

在网络上有大量关于事件循环及其工作原理的介绍,新的文章也不断涌现。不幸的是,但是这些材料中提供的信息并非都经过验证或可靠的。最终,这个概念本身已被一些神话和猜测所围绕。有时,即使是经验丰富的开发人员也需要付出大量的注意力和经验,才能辨别真相与虚构之间的区别。

事件循环Event LoopJavaScript 中是否存在?

事件循环Event Loop确实存在。但是,它在ECMA-262规范中找不到。事件循环不是 JavaScript(ECMAScript)语言的一部分,因此,不受其规范的管辖。该术语存在于 HOST 执行器领域,在其中的JavaScript引擎的具体实现可以自行决定是否利用事件循环作为执行环境的 API

事件循环的官方信息来源是什么?

正如我们之前提到的,ECMA-262 规范中没有提到事件循环一词,因为它超出了语言的范围,但属于负责执行 JavaScript 代码的 HOST 执行器的领域。因此,关于事件循环的信息应该从管理或记录这个实现环境的来源中寻找。今天有许多这样的环境,可以大致分为基于浏览器和非基于浏览器的环境。

浏览器的环境

这类环境的机制由 WHATWG 组织通过 HTML 规范进行管理。

具体来说,在规范的第 8.1.7 节“事件循环”中涉及了事件循环。我们稍后将讨论 Web API 中的事件循环的算法和标准。现在值得一提的是,浏览器通常依赖于执行它们的操作系统的 API;例如,macOS 中的 Chromium 依赖于 NSRunLoop,在 Linux 中,它依赖于 glib

这里的一个例外是 Electron,由于其所谓的跨平台性质,遇到了在不同操作系统上实现事件循环的挑战,因此过渡到使用libuv库,类似于 Node.js(稍后会详细介绍)。

非浏览器的环境

在非浏览器的环境中,正如名称所示,HTML 标准没有被实现。由于除 ECMA-262 外,这类环境应该如何运行没有国际标准和规范,因此它们自己的文档是唯一的官方信息来源。

迄今为止,最常见的非浏览器环境是 Node.js

Node.js 文档中,有一个关于 libuv 事件循环的章节,描述了Node API中唯一可用的函数 napi_get_uv_event_loop,旨在获取对当前事件循环的引用。

不幸的是,这个文档没有提供事件循环的其他描述。但是,很明显,为了确保其运行,Node.js 使用了 libuv 库,这个库专门为 Node.js 开发,为此环境提供了事件循环的实现。现在,该库也被一些其他项目使用。该库的内部文档包含 uv_loop_t API 部分,提供了事件循环的正式API规范。此外,文档还包含了事件循环的示意图以及在该库中使用事件循环的指南。

其他非浏览器环境,如 DenoBun,也依赖于libuv库来处理事件循环。移动环境,包括 React NativeNativeScriptApache Cordova,也是非基于浏览器的。它们依赖于执行它们的操作系统的 API。例如,对于 Android,这是 Android.os.Looper,而对于 iOS,这是 RunLoop

ECMA-262 规范如何定义事件循环

如果没有了事件循环机制,JavaScript 代码的执行将变得极其困难。ECMA-262 规范怎么会忽视这么重要的方面呢?

尽管 ECMA-262 规范中没有包括事件循环一词,但这并不意味着它没有以任何方式规范代码执行过程。然而,这种规范不集中在一个具体的概念下。总的来说,整个“可执行代码和执行上下文”(Executable Code and Execution Contexts)的第 9 节都致力于 JavaScript 执行过程。在这个部分中,第 9.7 条“Agents”引入了“Agent”一词,并提供了 Agent Record 的结构,其中包括负责阻塞该代理的字段。代理的实现仍然由 HOST 执行器负责,但有一些限制。具体而言,第 9.9 节“Forward Progress”概述了代理实现的基本要求:

  • 当其正在运行的执行上下文同步且无限期地等待外部事件时,代理会变为阻塞状态
  • 只有其 Agent Record [[CanBlock]] 字段为 true 的代理才能以这种方式变为阻塞状态
  • 一个未阻塞的代理是一个没有被阻塞的代理
  • 每个具有专用执行线程的未阻塞代理最终都会向前进展
  • 在共享执行线程的一组代理中,一个代理最终会向前进展
  • 一个代理不会使另一个代理变为阻塞状态,除非通过明确提供阻塞的显式 API

这些限制,加上第 29 节“Memory Model”中的保证,足以使所有 SEQ-CST 记录最终对所有代理可观察到。

HTML 规范中对事件循环的描述可以被视为参考吗?

由于官方的事件循环规范仅存在于HTML标准中,因此有相当多的非浏览器变体。这些都是由开发人员自行决定开发的,每种都有自己的特点。对于每种变体都需要一个单独的文章(一些已经在线上可用)。此外,许多实现在某种程度上或多或少地依赖于HTML规范,以防止重复造轮子,这是合理的。

是否将HTML规范视为事件循环部分的参考是一个有争议的问题。这个问题没有明确的答案,但考虑到以上内容,从现在开始,我们将运用这个规范来进一步考虑这个问题。

事件循环只针对同步和异步的 JavaScript 操作

正如我们之前提到的,事件循环不属于 JavaScript 语言领域。对于JavaScript来说,它是一种外部机制(如果你愿意,可以称之为“服务”),它允许你组织你的工作。与此同时,事件循环本身并不仅限于执行JS代码。事件循环负责许多进程,如输入/输出操作(鼠标和键盘事件,文件读写等)、事件协调、渲染、网络操作等等。

事件循环是否线程安全?

这个问题非常有趣。早些时候,我们讨论了 ECMA-262 规范中的“9.9 Forward Progress”,该节对代理的实现设置了一定的限制。该部分并没有明确指出线程安全性。相反,它指出,如果同一个线程中有多个代理,只有一个代理应该进展。这个模型清楚地表明,不需要线程安全性,因为一次只有一个代理可以工作。
在大多数情况下,是的。例如,Node.js 中使用的libuv库明确说明它们的实现不是线程安全的,它们的事件循环应该在单线程中使用,或者以多线程模式独立组织工作。
然而,对于浏览器实现来说,情况并不那么简单。
首先,值得澄清的是,在 HTML 规范的第 8.1.2 节“代理和代理集群”中,规范标识了几种类型的代理:

  • 类似源窗口代理 : 包含各种Window对象,这些对象可以直接或通过使用 document.domain 相互访问
  • 专用工作线程代理 : 包含单个 DedicatedWorkerGlobalScope(具有与之关联的隐式 MessagePort 的范围)
  • 共享工作线程代理 : 包含单个 SharedWorkerGlobalScope(具有构造函数来源、构造函数 URL 和凭证)
  • 服务工作线程代理 : 包含单个 SharedWorkerGlobalScope(具有关联的服务工作线程)
  • Worklet 代理 : 包含单个 WorkletGlobalScope(由于 worklets 可以导入 ES 模块,所以此范围具有关联的模块映射)

根据代理的类型,规范标识了三种类型的事件循环:

  • 窗口事件循环 - 用于类似源窗口代理
  • 工作线程事件循环 : 用于专用工作线程代理、共享工作线程代理和服务工作线程代理
  • worklet事件循环 : 用于Worklet代理

在这些中,工作线程事件循环和worklet事件循环的代理标志[[CanBlock]]设置为 true,迫使它们遵循“9.9 Forward Progress”的限制。因此,这样的事件循环将在它们自己的专用线程中工作。
另一方面,可以在同一个线程中同时使用多个窗口事件循环(例如,如果浏览器开发人员希望,几个浏览器选项卡可以共享一个线程)。

事件循环由宏任务和微任务组成的吗?

不完全正确。规范中实际上不存在“宏任务”这个术语。实际上,事件循环由任务队列和微任务队列组成,它们的机制基本上是不同的。
值得注意的是,与其名称相反,任务队列在技术上不是队列;它实际上是一个集合。相比之下,微任务队列确实是一个队列。这导致了一个重要的区别:在下一次迭代开始时,任务队列可能包含不同状态的许多任务。传统上,队列算法假定第一个元素从队列中移除(出队)。然而,对于任务队列,第一个元素在特定时刻不一定是可运行的任务。与标准队列算法不同,该过程不是简单的出队操作,而是要定位可运行任务状态的第一个任务,并从集合中提取它。另一方面,微任务被放入队列,并按照它们被添加的顺序进行移除。对这一过程的更深入的了解将在下面详细介绍。

任务队列中包含哪些内容?

在这个问题上,通常会出现认知上的不一致。一方面,任务队列用于延迟任务处理,即异步执行。但是同步代码会发生什么?为了解决这个问题,值得在 JavaScript 之外稍作深入(因为我们已经知道事件循环是在JavaScript之外运行的),并意识到对于浏览器来说,JS 代码本身只是它处理的众多实体之一。通过解析脚本文件或 <script> 标签,浏览器会收到一个标记化的结果。这个标记化结果的完成本身就成为一个独立的任务,在事件循环中生成一个全局任务类型的任务。因此,实际上,同步代码从执行的一开始就已经以任务的形式存在于事件循环中。
此外,随着脚本的执行,会生成新的任务,并放置在同一个事件循环中。
还有哪些内容会进入任务队列?

  • Events : 将事件对象分派给特定的 EventTarget 对象通常由专用任务执行,但并非总是如此。许多事件会在其他任务中分派。例如,MouseEventKeyboardEvent 事件可以合并为一个任务,其源与用户交互任务源相关联
  • Parsing : HTML 解析器标记化一个或多个字节,然后处理任何生成的标记,通常是一个任务
  • Callbacks : 通常,它们会落入任务队列,setTimeout(() => {}) 的经典示例也是如此,在这种情况下,一个callback被传递给 setTimeout,它将成为任务队列中的一个单独任务
  • 使用资源 - 在非阻塞资源获取的情况下(例如图像),会在任务队列中设置一个单独的任务
  • 响应 DOM 操作 - 一些元素在响应 DOM 操作时会在任务队列中生成一个任务。举例来说,向 DOM 插入一个新元素会触发重新渲染父元素的任务

微任务队列有什么目的?哪些任务是微任务?

在任务队列的情况下,定义和添加任务到队列的责任在于代理。微任务队列是运行时通过 Web API 提供给任务的一个选项,使任务能够满足其自身的异步需求。技术上讲,无论是执行 JavaScript 代码、标记化 HTML 文本、处理 I/O 事件还是其他操作,每个任务都可以使用微任务队列来实现其目标。
具体来看 JavaScript,该语言假设存在其自身的异步操作,这些操作不受 HTML 规范的约束。这些操作最好通过 Promise 进行说明。更具体地说,规范的第 27.2.1.3.2 节“Promise 解析函数”描述了解析 promise 的过程。第 15 步涉及执行 HostEnqueuePromiseJob,其实现位于 HOST 执行器中,但遵循某些要求,例如按照调度它们的 HostEnqueuePromiseJobs 的顺序运行作业。
如前所述,HostEnqueuePromiseJobs 机制的实现完全由 HOST 执行器负责。然而,在这种情况下使用微任务队列似乎是非常合理的。为了进一步澄清,让我们参考其中一个最广泛使用的JavaScript引擎 - V8 的源代码
/src/objects/objects.cc#4839

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/ 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,引擎将相应的微任务放入了微任务队列,从而证实了我们的假设。

事件循环如何运作的?

如上所述,有许多实现算法的变体。基于浏览器的环境遵循 HTML 规范,并且通常依赖于执行环境的操作系统 API 来实现机制。在这种情况下,值得在每个具体浏览器引擎的源代码中,以及相应的库和操作系统 API 中寻找实现示例。在使用第三方库(例如 libuv)的平台上,可以在该库本身中找到实现示例(libuv 是开源的)。然而,应该理解每个实现都是独立的,可能与其他实现有很大的差异。

为了举例说明,并且不依赖于任何特定的实现,让我们尝试描绘我们伪版本的机制。鉴于我们在 JavaScript 的背景下进行讨论,为了易于理解和理解,我们将在 TypeScript 中实现它。

以下清单显示了事件循环操作所必需的接口。这些接口仅用于演示目的,符合 HTML 规范,并且以任何真实 HOST 执行器的内部结构。事件循环算法本身将在下面给出。

如前所述,有几种实现此算法的变体。基于浏览器的环境遵循 HTML 规范,并且通常依赖于操作系统 API 进行实际实现。在这方面,建议在每个具体浏览器引擎的源代码中以及相应的库和操作系统 API 中寻找实现示例。在使用第三方库的平台上,如 libuv,有利于在库本身内检查实现示例(libuv 是开源的)。然而,重要的是要注意,每个实现都是独立的,并且可能与其他实现有很大的不同。

为了举例说明,并试图不依赖于任何特定的实现,让我们考虑机制的伪版本。考虑到我们在JavaScript的背景下讨论,为了理解和清晰起见,我们将在 TypeScript 中实现此功能。

以下清单显示了事件循环操作所必需的接口。需要注意的是,这些接口纯粹作为演示提供,符合 HTML 规范,并且不反映任何真实 HOST 执行器的内部结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
* 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;
}

我们已经描述了必要的接口,至少是我们需要理解下面算法的部分。现在,让我们来看一下事件循环算法本身。需要澄清的是,该算法是对 8.1.7.3 处理模型规范的说明,不以任何方式反映 HOST 执行器上的实际实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* 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 执行微任务检查点已被转移到一个名为 performMicrotaskCheckpoint 的单独函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/** 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.
}

总结:

  • 事件循环存在,但超出了 ECMA-262 规范的责任范围。
  • 在浏览器的环境中,事件循环的官方信息来源可以被认为是 HTML 规范;
  • 在非浏览器的环境中,官方文档或者 HOST 执行器自身的官方文档可以被认为是事件循环的官方信息来源。
  • ECMA-262 规范间接涉及与事件循环相关的流程,将这些流程的实现留给了 HOST 执行器自行决定。
  • 事件循环并不仅仅与维护 JavaScript 代码有关。事实上,JavaScript 只是可以进入事件循环的任务类型之一。除了 JavaScript,浏览器还可以将其他任务放置在这里,例如标记接收到的 HTML 文本、处理输入/输出操作、在屏幕上渲染元素等等。
  • 根据 HTML 规范,事件循环不必是线程安全的,但可以是。在执行多个代理并将它们的事件循环放置在同一个线程的情况下,它们必须组织彼此之间的交互算法,以确保在任一时刻只有一个代理作为未阻塞代理出现,而其他代理必须处于阻塞状态。
  • 事件循环由任务队列和微任务队列组成。由 HOST 执行器分配的任务被放置到任务队列中。微任务队列是任务队列中的任务使用执行器的 Web API 执行其特定的异步子任务的一个可选机会。