在网络上有大量关于事件循环及其工作原理的介绍,新的文章也不断涌现。不幸的是,但是这些材料中提供的信息并非都经过验证或可靠的。最终,这个概念本身已被一些神话和猜测所围绕。有时,即使是经验丰富的开发人员也需要付出大量的注意力和经验,才能辨别真相与虚构之间的区别。
事件循环Event Loop
在 JavaScript
中是否存在?
事件循环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
规范。此外,文档还包含了事件循环的示意图以及在该库中使用事件循环的指南。
其他非浏览器环境,如
Deno
和 Bun
,也依赖于libuv
库来处理事件循环。移动环境,包括React Native
、NativeScript
和 Apache 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
对象通常由专用任务执行,但并非总是如此。许多事件会在其他任务中分派。例如,MouseEvent
和KeyboardEvent
事件可以合并为一个任务,其源与用户交互任务源相关联 - 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
/ 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 执行器的内部结构。
/**
* 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 执行器上的实际实现。
/**
* 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 的单独函数中。
/** 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 执行其特定的异步子任务的一个可选机会。
网友评论