美文网首页
Event Loop:事件循环的神话与现实

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

作者: 涅槃快乐是金 | 来源:发表于2024-02-23 17:31 被阅读0次

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

    事件循环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

    / 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 执行其特定的异步子任务的一个可选机会。

    相关文章

      网友评论

          本文标题:Event Loop:事件循环的神话与现实

          本文链接:https://www.haomeiwen.com/subject/viawadtx.html