Skip to content

Instantly share code, notes, and snippets.

@YieldRay
Last active July 20, 2024 12:42
Show Gist options
  • Save YieldRay/258504dbb9869cc864239bf46b8136bb to your computer and use it in GitHub Desktop.
Save YieldRay/258504dbb9869cc864239bf46b8136bb to your computer and use it in GitHub Desktop.

8.1.7 事件循环

8.1.7.1 定义

为了协调事件、用户交互、脚本、渲染、网络以及其他方面,用户代理必须使用本节所述的事件循环。每个 代理 都有一个关联的事件循环,该循环对该代理是唯一的。

同源窗口代理事件循环 被称为窗口事件循环。专用工作者代理共享工作者代理服务工作者代理事件循环 被称为工作者事件循环。Worklet 代理事件循环 被称为 Worklet 事件循环。

事件循环 不一定对应于实现线程。例如,多个 窗口事件循环 可以在单个线程中协作调度。

但是,对于 [[CanBlock]] 设置为 true 的各种工作者 代理,JavaScript 规范确实对它们提出了关于 前进进度 的要求,这实际上相当于在这些情况下需要专用的代理线程。


事件循环 有一个或多个任务队列。任务队列 是一个 任务集合

任务队列集合,而不是 队列,因为 事件循环处理模型 从所选队列中获取第一个 可运行任务,而不是将第一个任务 出队

微任务队列 不是 任务队列

任务封装了负责以下工作的算法:

  • 事件

    在特定 EventTarget 对象上调度 Event 对象通常由专用任务完成。

    并非所有事件都使用 任务队列 进行调度;许多事件是在其他任务期间调度的。

  • 解析

    HTML 解析器 对一个或多个字节进行标记化,然后处理任何生成的标记,通常是一个任务。

  • 回调

    调用回调通常由专用任务完成。

  • 使用资源

    当算法 获取 资源时,如果获取以非阻塞方式发生,则在部分或全部资源可用后对资源的处理由任务执行。

  • 对 DOM 操作做出反应

    某些元素具有响应 DOM 操作而触发的任务,例如当该元素被 插入文档 中时。

形式上,任务是一个 结构,它具有:

  • 步骤

    一系列步骤,指定任务要完成的工作。

  • 来源

    任务来源 之一,用于对相关任务进行分组和序列化。

  • 文档

    与任务关联的 Document,或者对于不在 窗口事件循环 中的任务,则为 null。

  • 脚本评估环境设置对象集

    用于在任务期间跟踪脚本评估的 环境设置对象集合

如果 任务文档 为 null 或 完全活动,则该任务是可运行的。

根据其 来源 字段,每个 任务 都被定义为来自特定的任务来源。对于每个 事件循环,每个 任务来源 必须与特定的 任务队列 相关联。

本质上,标准中使用 任务来源 来分离逻辑上不同类型的任务,用户代理可能希望区分这些任务。用户代理使用 任务队列 来合并给定 事件循环 中的任务来源。

例如,用户代理可以为鼠标和键盘事件(与之关联的是 用户交互任务来源)使用一个 任务队列,而所有其他 任务来源 都关联到另一个任务队列。然后,利用 事件循环处理模型 第一步中赋予的自由,它可以在四分之三的时间内优先处理键盘和鼠标事件,而不是其他任务队列,从而保持界面的响应速度,但不会使其他任务队列饿死。请注意,在此设置中,处理模型仍然强制用户代理永远不会无序地处理来自任何一个 任务来源 的事件。


每个 事件循环 都有一个当前正在运行的任务,它是一个 任务 或 null。最初,它是 null。它用于处理重入。

每个 事件循环 都有一个微任务队列,它是 微任务队列,最初为空。微任务是一种口语化的说法,指的是通过 排队微任务 算法创建的 任务

每个 事件循环 都有一个执行微任务检查点的布尔值,最初为 false。它用于防止 执行微任务检查点 算法的重入调用。

每个 窗口事件循环 都有一个 DOMHighResTimeStamp 类型的最后渲染机会时间,最初设置为零。

每个 窗口事件循环 都有一个 DOMHighResTimeStamp 类型的最后空闲时间段开始时间,最初设置为零。

要获取 窗口事件循环 loop 的同循环窗口,请返回所有其 相关代理事件循环loopWindow 对象。

8.1.7.2 任务排队

要在 任务来源 source 上排队一个任务,该任务执行一系列步骤 steps,可以选择提供一个事件循环 event loop 和一个文档 document

  1. 如果未提供 event loop,则将 event loop 设置为 隐含事件循环

  2. 如果未提供 document,则将 document 设置为 隐含文档

  3. 创建一个新的 任务 task

  4. task步骤 设置为 steps

  5. task来源 设置为 source

  6. task文档 设置为 document

  7. task脚本评估环境设置对象集 设置为空 集合

  8. queueevent loopsource 关联到的 任务队列

  9. task 追加queue

未能将事件循环和文档传递给 任务排队 意味着依赖于不明确且规范不佳的 隐含事件循环隐含文档 概念。规范作者应该始终传递这些值,或者使用包装器算法 排队全局任务排队元素任务 来代替。建议使用包装器算法。

要在 任务来源 source 上排队一个全局任务,该任务具有 全局对象 global 和一系列步骤 steps

  1. event loopglobal相关代理事件循环

  2. 如果 globalWindow 对象,则令 documentglobal关联 Document;否则为 null。

  3. 排队一个任务,给定 sourceevent loopdocumentsteps

要在 任务来源 source 上排队一个元素任务,该任务具有元素 element 和一系列步骤 steps

  1. globalelement相关全局对象

  2. 排队一个全局任务,给定 sourceglobalsteps

要排队一个执行一系列步骤 steps 的微任务,可以选择提供一个文档 document

  1. 断言: 存在 周围代理。也就是说,此算法在 并行 时不会被调用。

  2. eventLoop周围代理事件循环

  3. 如果未提供 document,则将 document 设置为 隐含文档

  4. 创建一个新的 任务 microtask

  5. microtask步骤 设置为 steps

  6. microtask来源 设置为微任务任务来源。

  7. microtask文档 设置为 document

  8. microtask脚本评估环境设置对象集 设置为空 集合

  9. eventLoop微任务队列入队 microtask

如果 微任务 在其初始执行期间 旋转事件循环,则可以将其移动到常规 任务队列。这是唯一一种会参考微任务的 来源文档脚本评估环境设置对象集 的情况;执行微任务检查点 算法会忽略它们。

任务排队时的隐含事件循环是可以从调用算法的上下文中推断出来的。这通常是明确的,因为大多数规范算法只涉及单个 代理(因此是单个 事件循环)。例外情况是涉及或指定跨代理通信的算法(例如,窗口和工作者之间);对于这些情况,不能依赖 隐含事件循环 概念,规范必须在 任务排队 时显式提供 事件循环

事件循环 event loop 上排队任务时的隐含文档确定如下:

  1. 如果 event loop 不是 窗口事件循环,则返回 null。

  2. 如果任务是在元素的上下文中排队的,则返回元素的 节点文档

  3. 如果任务是在 浏览上下文 的上下文中排队的,则返回浏览上下文的 活动文档

  4. 如果任务是由或为 脚本 排队的,则返回脚本的 设置对象全局对象关联 Document

  5. 断言: 永远不会到达此步骤,因为前面的条件之一为真。真的吗?

隐含事件循环隐含文档 的定义都很模糊,并且有很多超距作用。希望能够删除它们,特别是 隐含文档。请参阅 issue #4980

8.1.7.3 处理模型

事件循环 必须不断地运行以下步骤,直到它存在为止:

  1. oldestTasktaskStartTime 为 null。

  2. 如果 事件循环 具有至少一个 可运行任务任务队列,则:

    1. taskQueue 为其中一个 任务队列,以 实现定义 的方式选择。

      请记住,微任务队列 不是 任务队列,因此在此步骤中不会选择它。但是,可能会在此步骤中选择与 微任务任务来源 关联的 任务队列。在这种情况下,下一步中选择的 任务 最初是一个 微任务,但它作为 旋转事件循环 的一部分被移动了。

    2. taskStartTime 设置为 不安全的共享当前时间

    3. oldestTask 设置为 taskQueue 中第一个 可运行任务,并将其从 taskQueue移除

    4. 如果 oldestTask文档 不为 null,则 记录任务开始时间,给定 taskStartTimeoldestTask文档

    5. 事件循环当前正在运行的任务 设置为 oldestTask

    6. 执行 oldestTask步骤

    7. 事件循环当前正在运行的任务 设置回 null。

    8. 执行微任务检查点

  3. taskEndTime不安全的共享当前时间[HRT]

  4. 如果 oldestTask 不为 null,则:

    1. top-level browsing contexts 为空 集合

    2. 对于 oldestTask脚本评估环境设置对象集 中的每个 环境设置对象 settings

      1. globalsettings全局对象

      2. 如果 global 不是 Window 对象,则 继续

      3. 如果 global浏览上下文 为 null,则 继续

      4. tlbcglobal浏览上下文顶级浏览上下文

      5. 如果 tlbc 不为 null,则将其 追加top-level browsing contexts

    3. 报告长任务,传入 taskStartTimetaskEndTimetop-level browsing contextsoldestTask

    4. 如果 oldestTask文档 不为 null,则 记录任务结束时间,给定 taskEndTimeoldestTask文档

  5. 如果这是一个 窗口事件循环,并且在此 事件循环任务队列 中没有 可运行任务,则:

    1. 将此 事件循环最后空闲时间段开始时间 设置为 不安全的共享当前时间

    2. computeDeadline 为以下步骤:

      1. deadline 为此 事件循环最后空闲时间段开始时间 加上 50。

        未来 50 毫秒的上限是为了确保在人类感知的阈值内对新用户输入做出响应。

      2. hasPendingRenders 为 false。

      3. 对于此 事件循环同循环窗口 中的每个 windowInSameLoop

        1. 如果 windowInSameLoop动画帧回调映射为空,或者如果用户代理认为 windowInSameLoop 可能有待处理的渲染更新,则将 hasPendingRenders 设置为 true。

        2. timerCallbackEstimates获取 windowInSameLoop活动计时器映射 的值的結果。

        3. 对于 timerCallbackEstimates 中的每个 timeoutDeadline,如果 timeoutDeadline 小于 deadline,则将 deadline 设置为 timeoutDeadline

      4. 如果 hasPendingRenders 为 true,则:

        1. nextRenderDeadline 为此 事件循环最后渲染机会时间 加上(1000 除以当前刷新率)。

          刷新率可以是硬件或实现特定的。对于 60Hz 的刷新率,nextRenderDeadline 将比 最后渲染机会时间 晚约 16.67 毫秒。

        2. 如果 nextRenderDeadline 小于 deadline,则返回 nextRenderDeadline

      5. 返回 deadline

    3. 对于此 事件循环同循环窗口 中的每个 win,对 win 执行 开始空闲时间段算法,其中以下步骤:返回调用 computeDeadline 的结果,粗略化 给定 win相关设置对象跨源隔离功能[REQUESTIDLECALLBACK]

  6. 如果这是一个 工作者事件循环,则:

    1. 如果此 事件循环代理 的单个 领域全局对象受支持DedicatedWorkerGlobalScope,并且用户代理认为此时更新其渲染将是有益的,则:

      1. now 为给定 DedicatedWorkerGlobalScope当前高分辨率时间[HRT]

      2. 为该 DedicatedWorkerGlobalScope 运行动画帧回调,将 now 作为时间戳传入。

      3. 更新专用工作者的渲染以反映当前状态。

      类似于 窗口事件循环更新渲染 的注释,用户代理可以确定专用工作者中的渲染速率。

    2. 如果 事件循环任务队列 中没有 任务,并且 WorkerGlobalScope 对象的 关闭 标志为 true,则销毁 事件循环,中止这些步骤,恢复下面 Web 工作者 部分中描述的 运行工作者 步骤。

窗口事件循环 eventLoop 还必须 并行 运行以下内容,只要它存在:

  1. 等待至少一个 可导航活动文档相关代理事件循环eventLoop渲染机会

  2. eventLoop最后渲染机会时间 设置为 不安全的共享当前时间

  3. 对于每个具有 渲染机会navigable,在 渲染任务来源排队一个全局任务,给定 navigable活动窗口 以更新渲染:

    这可能会导致对 更新渲染 的冗余调用。但是,这些调用不会产生可观察到的效果,因为根据 不必要的渲染 步骤,将不需要渲染。实现可以引入进一步的优化,例如仅在尚未排队时才对该任务进行排队。但是,请注意,与任务关联的文档可能会在处理任务之前变为非活动状态。

    1. frameTimestampeventLoop最后渲染机会时间

    2. docs 为所有 相关代理事件循环eventLoop完全活动Document 对象,任意排序,但必须满足以下条件:

      在迭代 docs 的以下步骤中,必须按照在列表中找到每个 Document 的顺序对其进行处理。

    3. 过滤不可渲染的文档: 从 docs 中移除满足以下任一条件的任何 Document 对象 doc

      除了在 并行 步骤中进行检查外,我们还必须在此处检查渲染机会,因为共享同一个 事件循环 的某些文档可能不会同时具有 渲染机会

    4. 不必要的渲染: 从 docs 中移除满足以下所有条件的任何 Document 对象 doc

    5. docs 中移除用户代理认为出于其他原因最好跳过更新渲染的所有 Document 对象。

      标有 过滤不可渲染的文档 的步骤可防止用户代理在无法向用户呈现新内容时更新渲染。

      标有 不必要的渲染 的步骤可防止用户代理在没有要绘制的新内容时更新渲染。

      此步骤使用户代理能够出于其他原因阻止以下步骤运行,例如,为了确保某些 任务 在彼此之后立即执行,仅穿插 微任务检查点(而没有穿插 动画帧回调)。具体来说,用户代理可能希望将计时器回调合并在一起,而无需中间渲染更新。

    6. 对于 docs 中的每个 doc显示 doc

    7. 对于 docs 中的每个 doc,如果其 节点可导航顶级可遍历 的,则为 doc 刷新自动对焦候选

    8. 对于 docs 中的每个 doc,为 doc 运行调整大小步骤[CSSOMVIEW]

    9. 对于 docs 中的每个 doc,为 doc 运行滚动步骤[CSSOMVIEW]

    10. 对于 docs 中的每个 doc,为 doc 评估媒体查询并报告更改[CSSOMVIEW]

    11. 对于 docs 中的每个 doc,为 doc 更新动画并发送事件,将 相对高分辨率时间 给定 frameTimestampdoc相关全局对象 作为时间戳传入。[WEBANIMATIONS]

    12. 对于 docs 中的每个 doc,为 doc 运行全屏步骤[FULLSCREEN]

    13. 对于 docs 中的每个 doc,如果用户代理检测到与 CanvasRenderingContext2DOffscreenCanvasRenderingContext2Dcontext)关联的后备存储已丢失,则它必须为每个此类 context 运行上下文丢失步骤:

      1. canvascontextcanvas 属性的值(如果 contextCanvasRenderingContext2D),否则为 context关联 OffscreenCanvas 对象

      2. context上下文丢失 设置为 true。

      3. 给定 context将渲染上下文重置为其默认状态

      4. shouldRestore 为在 canvas触发 名为 contextlost 的事件的结果,该事件的 cancelable 属性初始化为 true。

      5. 如果 shouldRestore 为 false,则中止这些步骤。

      6. 尝试通过使用 context 的属性创建后备存储并将其与 context 关联来恢复 context。如果失败,则中止这些步骤。

      7. context上下文丢失 设置为 false。

      8. canvas触发 名为 contextrestored 的事件。

    14. 对于 docs 中的每个 doc,为 doc 运行动画帧回调,将 相对高分辨率时间 给定 frameTimestampdoc相关全局对象 作为时间戳传入。

    15. unsafeStyleAndLayoutStartTime不安全的共享当前时间

    16. 对于 docs 中的每个 doc

      1. resizeObserverDepth 为 0。

      2. 当 true 时:

        1. 重新计算样式并更新 doc 的布局。

        2. hadInitialVisibleContentVisibilityDetermination 为 false。

        3. 对于使用 'auto' 作为 'content-visibility' 的值的每个元素 element

          1. 如果 element到视口的距离 未确定并且它与用户 无关,则令 checkForInitialDetermination 为 true。否则,令 checkForInitialDetermination 为 false。

          2. 确定 element 到视口的距离

          3. 如果 checkForInitialDetermination 为 true 并且 element 现在与用户 相关,则将 hadInitialVisibleContentVisibilityDetermination 设置为 true。

        4. 如果 hadInitialVisibleContentVisibilityDetermination 为 true,则 继续

          此步骤的目的是为了使初始视口距离确定(立即生效)反映在此循环的先前步骤中执行的样式和布局计算中。初始视口距离确定以外的其他距离确定在下一个 渲染机会 生效。[CSSCONTAIN]

        5. doc 收集深度为 resizeObserverDepth活动大小调整观察

        6. 如果 doc 具有活动大小调整观察

          1. resizeObserverDepth 设置为给定 doc 广播活动大小调整观察 的结果。

          2. 继续

        7. 否则,中断

      3. 如果 doc 已跳过大小调整观察,则给定 doc 传递大小调整循环错误

    17. 对于 docs 中的每个 doc,如果 doc焦点区域 不是 可聚焦区域,则为 doc视口 运行 聚焦步骤,并将 doc相关全局对象导航 API在正在进行的导航期间焦点已更改 设置为 false。

      例如,这可能是因为某个元素添加了 hidden 属性,导致它停止 被渲染。当 input 元素被 禁用 时,也可能会发生这种情况。

      这将 通常 触发 blur 事件,并且可能触发 change 事件。

      除了这种异步修复之外,如果 文档的焦点区域 被移除,则还有一个 同步修复。该修复 不会 触发 blurchange 事件。

    18. 对于 docs 中的每个 doc,为 doc 执行待处理的转换操作[CSSVIEWTRANSITIONS]

    19. 对于 docs 中的每个 doc,为 doc 运行更新交叉观察步骤,将 相对高分辨率时间 给定 nowdoc相关全局对象 作为时间戳传入。[INTERSECTIONOBSERVER]

    20. 对于 docs 中的每个 doc,给定 unsafeStyleAndLayoutStartTimedoc 记录渲染时间

    21. 对于 docs 中的每个 doc,为 doc 标记绘制时间

    22. 对于 docs 中的每个 doc,更新 doc 及其 节点可导航 的渲染或用户界面以反映当前状态。

    23. 对于 docs 中的每个 doc,给定 doc 处理顶层移除

如果用户代理当前能够向用户呈现 可导航 的内容,则该 可导航 就有渲染机会,同时考虑硬件刷新率限制和用户代理为性能原因进行的限制,但即使内容在视口之外也认为内容是可呈现的。

可导航渲染机会 是根据硬件限制(例如显示刷新率)和其他因素(例如页面性能或其 活动文档可见性状态 是否为 "visible")确定的。渲染机会通常以固定的时间间隔出现。

本规范没有强制要求任何特定的模型来选择渲染机会。但例如,如果浏览器试图实现 60Hz 的刷新率,则渲染机会最多每秒出现 60 次(约 16.7 毫秒)。如果浏览器发现 可导航 无法维持此速率,则它可能会降低到该 可导航 每秒 30 次渲染机会,而不是偶尔丢帧。类似地,如果 可导航 不可見,则用户代理可以决定将该页面降低到每秒 4 次渲染机会,甚至更少。


当用户代理要执行微任务检查点时:

  1. 如果 事件循环执行微任务检查点 为 true,则返回。

  2. 事件循环执行微任务检查点 设置为 true。

  3. 事件循环微任务队列为空 时:

    1. oldestMicrotask 为从 事件循环微任务队列 出队 的结果。

    2. 事件循环当前正在运行的任务 设置为 oldestMicrotask

    3. 运行 oldestMicrotask

      这可能涉及调用脚本化的回调,最终调用 运行脚本后的清理 步骤,该步骤再次调用此 执行微任务检查点 算法,这就是我们使用 执行微任务检查点 标志来避免重入的原因。

    4. 事件循环当前正在运行的任务 设置回 null。

  4. 对于每个 责任事件循环 为此 事件循环环境设置对象,在该 环境设置对象通知已拒绝的 Promise

  5. 清理 IndexedDB 事务

  6. 执行 ClearKeptObjects().

    WeakRef.prototype.deref() 返回一个对象时,该对象会一直保持活动状态,直到下次调用 ClearKeptObjects(),之后它将再次进行垃圾回收。

  7. 事件循环执行微任务检查点 设置为 false。

  8. 记录微任务检查点的计时信息


并行 运行的算法要等待稳定状态时,用户代理必须 排队一个微任务 来运行以下步骤,然后必须停止执行(算法的执行在微任务运行时恢复,如以下步骤所述):

  1. 运行算法的同步部分。

  2. 如算法步骤中所述,并行 恢复算法的执行(如果适用)。

同步部分 中的步骤用 标记。


算法步骤中说要旋转事件循环直到满足条件 goal,这等效于替换以下算法步骤:

  1. task事件循环当前正在运行的任务

    task 可以是 微任务

  2. task sourcetask来源

  3. old stackJavaScript 执行上下文堆栈 的副本。

  4. 清空 JavaScript 执行上下文堆栈

  5. 执行微任务检查点

    如果 task微任务,则此步骤将是无操作的,因为 执行微任务检查点 为 true。

  6. 并行

    1. 等待,直到满足条件 goal

    2. task source排队一个任务,以:

      1. JavaScript 执行上下文堆栈 替换为 old stack

      2. 执行原始算法中此 旋转事件循环 实例之后出现的任何步骤。

        这将恢复 task

  7. 停止 task,允许调用它的任何算法恢复。

    这将导致 事件循环 的主要步骤集或 执行微任务检查点 算法继续。

与本规范和其他规范中的其他算法(其行为类似于编程语言函数调用)不同,旋转事件循环 更像是一个宏,它通过扩展成一系列步骤和操作来节省使用站点上的输入和缩进。

其步骤如下所示的算法:

  1. 做某事。

  2. 旋转事件循环,直到发生很棒的事情。

  3. 做其他事情。

是一个简写,在“宏扩展”之后变成

  1. 做某事。

  2. old stackJavaScript 执行上下文堆栈 的副本。

  3. 清空 JavaScript 执行上下文堆栈

  4. 执行微任务检查点

  5. 并行

    1. 等待,直到发生很棒的事情。

    2. 在执行“做某事”的任务来源上 排队一个任务,以:

      1. JavaScript 执行上下文堆栈 替换为 old stack

      2. 做其他事情。

下面是一个更完整的替换示例,其中事件循环是从并行工作中排队的任务内部旋转的。使用 旋转事件循环 的版本:

  1. 并行

    1. 做并行的事情 1。

    2. DOM 操作任务来源排队一个任务,以:

      1. 做任务的事情 1。

      2. 旋转事件循环,直到发生很棒的事情。

      3. 做任务的事情 2。

    3. 做并行的事情 2。

完全扩展的版本:

  1. 并行

    1. 做并行的事情 1。

    2. old stack 为 null。

    3. DOM 操作任务来源排队一个任务,以:

      1. 做任务的事情 1。

      2. old stack 设置为 JavaScript 执行上下文堆栈 的副本。

      3. 清空 JavaScript 执行上下文堆栈

      4. 执行微任务检查点

    4. 等待,直到发生很棒的事情。

    5. DOM 操作任务来源排队一个任务,以:

      1. JavaScript 执行上下文堆栈 替换为 old stack

      2. 做任务的事情 2。

    6. 做并行的事情 2。


由于历史原因,本规范中的一些算法要求用户代理在运行 任务 时暂停,直到满足条件 goal。这意味着运行以下步骤:

  1. global当前全局对象

  2. timeBeforePause 为给定 global当前高分辨率时间

  3. 如有必要,更新任何 Document可导航 的渲染或用户界面以反映当前状态。

  4. 等待,直到满足条件 goal。当用户代理有一个已暂停的 任务 时,相应的 事件循环 不得运行更多 任务,并且当前正在运行的 任务 中的任何脚本都必须阻塞。但是,用户代理在暂停时应保持对用户输入的响应,尽管由于 事件循环 不执行任何操作,因此响应能力会降低。

  5. 给定从 timeBeforePause 到给定 global当前高分辨率时间持续时间记录暂停持续时间

暂停 对用户体验非常不利,尤其是在多个文档共享单个 事件循环 的情况下。鼓励用户代理尝试 暂停 的替代方法,例如 旋转事件循环,或者甚至根本不进行任何类型的暂停执行,只要在保持与现有内容兼容的同时这样做是可能的。如果发现影响较小的替代方案与 Web 兼容,本规范将很乐意进行更改。

在此期间,实现者应该意识到,用户代理可能尝试的各种替代方案可能会改变 事件循环 行为的细微方面,包括 任务微任务 计时。即使这样做会导致它们违反 暂停 操作所暗示的精确语义,实现也应该继续进行试验。

8.1.7.4 通用任务来源

本规范和其他规范中的许多功能都使用以下 任务来源

8.1.7.5 处理来自其他规范的事件循环

编写与 事件循环 正确交互的规范可能很棘手。本规范使用独立于并发模型的术语,这加剧了这种情况,因此我们使用“事件循环”和“并行”之类的说法,而不是使用更熟悉的特定于模型的术语,例如“主线程”或“后台线程”。

默认情况下,规范文本通常在 事件循环 上运行。这源于正式的 事件循环处理模型,因为您可以最终将大多数算法追溯到在那里 排队任务

任何 JavaScript 方法的算法步骤都将由调用该方法的作者代码调用。作者代码只能通过排队的任务运行,这些任务通常来自 script 处理模型 中的某个位置。

从这个起点开始,首要的准则是,规范需要执行的任何可能会阻塞 事件循环 的工作都必须 并行 执行。这包括(但不限于):

  • 执行大量计算;

  • 显示面向用户的提示;

  • 执行可能需要涉及外部系统(即“进程外”)的操作。

接下来的复杂之处在于,在 并行 的算法部分中,您不得创建或操作与特定 领域全局环境设置对象 关联的对象。(用更熟悉的术语来说,您不得直接从后台线程访问主线程的项目。)这样做会创建 JavaScript 代码可观察到的数据竞争,因为毕竟您的算法步骤是 并行 于 JavaScript 代码运行的。

但是,您可以操作来自 Infra 的规范级数据结构和值,因为它们与领域无关。它们永远不会在没有发生特定转换的情况下直接暴露给 JavaScript(通常 通过 Web IDL)。[INFRA] [WEBIDL]

为了影响可观察到的 JavaScript 对象的世界,您必须 排队一个全局任务 来执行任何此类操作。这可以确保您的步骤相对于 事件循环 上发生的其他事情正确交错。此外,您必须在 排队全局任务 时选择一个 任务来源;这决定了您的步骤相对于其他步骤的相对顺序。如果您不确定要使用哪个 任务来源,请选择一个听起来最适用的 通用任务来源。最后,您必须指明您的排队任务与哪个 全局对象 关联;这可以确保如果该全局对象处于非活动状态,则该任务不会运行。

排队全局任务 所基于的基本原语是 排队任务 算法。一般来说,排队全局任务 更好,因为它会自动选择正确的 事件循环,并在适当的情况下选择 文档。旧规范通常将 排队任务隐含事件循环隐含文档 概念结合使用,但不鼓励这样做。

综上所述,我们可以为需要异步执行工作的典型算法提供一个模板:

  1. 事件循环 上进行任何同步设置工作。这可能包括将 领域 特定的 JavaScript 值转换为与领域无关的规范级值。

  2. 并行 执行一组可能代价高昂的步骤,完全对与领域无关的值进行操作,并产生与领域无关的结果。

  3. 在指定的 任务来源 上并给定适当的 全局对象排队一个全局任务,以将与领域无关的结果转换回对 事件循环 上可观察到的 JavaScript 对象世界的可观察到的影响。

以下是一个算法,它在将传入的 标量值字符串 列表 input 解析为 URL 后对其进行“加密”:

  1. urls 为一个空的 列表

  2. 对于 input 中的每个 string

    1. parsed 为相对于 当前设置对象 编码解析 URL 给定 string 的结果。

    2. 如果 parsed 是失败的,则返回 一个被拒绝的 Promise,并带有一个 "SyntaxError" DOMException

    3. serialized 为对 parsed 应用 URL 序列化器 的结果。

    4. serialized 追加urls

  3. realm当前领域

  4. p 为一个新的 Promise。

  5. 并行 运行以下步骤:

    1. encryptedURLs 为一个空的 列表

    2. 对于 urls 中的每个 url

      1. 等待 100 毫秒,以便人们认为我们正在进行繁重的加密工作。

      2. encrypted 为从 url 派生的新 字符串,其第 n代码单元 等于 url 的第 n代码单元 加 13。

      3. encrypted 追加encryptedURLs

    3. 网络任务来源排队一个全局任务,给定 realm全局对象,以执行以下步骤:

      1. array 为在 realm 中将 encryptedURLs 转换 为 JavaScript 数组的结果。

      2. 使用 array 解析 p

  6. 返回 p

关于此算法,需要注意以下几点:

  • 它在进入 并行 步骤之前,在 事件循环 上预先进行 URL 解析。这是必要的,因为解析取决于 当前设置对象,该对象在进入 并行 后将不再是当前的。

  • 或者,它可以保存对 当前设置对象API 基本 URL 的引用,并在 并行 步骤中使用它;这将是等效的。但是,我们建议改为预先完成尽可能多的工作,就像本例一样。尝试保存正确的值容易出错;例如,如果我们只保存了 当前设置对象,而不是其 API 基本 URL,则可能会出现竞争条件。

  • 它隐式地将 字符串 列表 从初始步骤传递到 并行 步骤。这是可以的,因为 列表字符串 都与 领域 无关。

  • 它在 并行 步骤中执行“昂贵的计算”(每个输入 URL 等待 100 毫秒),因此不会阻塞主 事件循环

  • Promise 作为可观察到的 JavaScript 对象,永远不会在 并行 步骤中创建和操作。p 是在进入这些步骤之前创建的,然后在专门为此目的 排队任务 中进行操作。

  • JavaScript 数组对象的创建也发生在排队的任务期间,并且需要注意指定在哪个领域中创建数组,因为这在上下文中不再明显。

(关于最后两点,另请参阅 whatwg/webidl issue #135whatwg/webidl issue #371,我们仍在仔细研究上述 Promise 解析模式的细节。)

还要注意的另一件事是,如果从 Web IDL 指定的操作(采用 sequence<USVString>)调用此算法,则会自动将作者作为输入提供的 领域 特定的 JavaScript 对象转换为与领域无关的 sequence<USVString> Web IDL 类型,然后我们将其视为 标量值字符串列表。因此,根据您的规范的结构方式,在主 事件循环 上可能会发生其他隐式步骤,这些步骤在您准备好进入 并行 的整个过程中发挥作用。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment