8.1.4 Event loops 

8.1.4.1 Definitions

为了协调事件,用户交互,脚本,渲染,网络等,用户代理必须使用本文所述的事件循环。有两种事件循环:用于浏览上下文(browsing contexts)Worker

在将来, 该标准希望能明确定义event loop 何时被创建或重用。

一个 window event loop相似源窗口代理使用的事件循环 . 用户代理可以跨 相似源窗口代理共享事件循环。

该规范并未描述如何处理由相似源窗口代理 之间导航所引起的复杂情况。例如, 当 浏览器上下文https://example.com/ 导航到 https://shop.example/.

每个用户代理必须至少有一个浏览上下文事件循环,并且至多有一个unit of related similar-origin browsing contexts(没查到中文这叫啥,字面我自行翻译成相似来源相关浏览上下文单元,该概念可以看这里)。

对于有多个相关浏览上下文单元事件循环的情况,当该组中的浏览上下文被导航,从一个相关的相似来源浏览上下文单元切换到另一个单元时,复杂性会出现。 本规范目前没有描述如何处理这些复杂问题。

浏览上下文事件循环始终至少有一个浏览上下文。如果这样的事件循环的浏览上下文全部消失,那么事件循环也会消失。浏览上下文总是有一个事件循环来协调其活动。

Worker事件循环更简单:每个Worker都有一个事件循环,通过Worker处理模型 ( worker processing model )管理事件循环的生命周期。

事件循环具有一个或多个任务队列 ( task queue )。任务队列是一个有序的任务列表,它是负责如下工作的算法:

  • 事件

    在特定的EventTarget对象上分派Event对象,通常由专门的任务完成。

并非所有事件都使用任务队列分派,其他许多事件都会在其他任务中分派。

  • Parsing

HTML解析器token一个或多个字节,然后处理任何结果令牌,这个过程一般视为一个任务。

  • Callbacks

调用回调通常由指定任务完成。

  • Using a resource

当算法获取资源时,如果以非阻塞方式进行提取,那么一旦某个或全部资源可用,对资源的处理由任务执行。

  • 对DOM操作作出反应

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

浏览上下文类型的事件循环中的每一个任务都与一个Document对象关联;如果任务的队列在一个元素的上下文中,那么它是元素的节点文档;如果任务队列在浏览上下文中,那么它是任务排队时的浏览上下文活动文档;如果任务是因为script脚本产生的队列,那么该文档对象是由该(script’s settings object)产生的(A responsible document)

A responsible document: For example, the URL of the responsible document is used to set the URL of the Document after it has been reset using document.open().

一个 任务(task) 旨在用于特定的事件循环:这个事件循环负责处理这些和Documenet以及Worker相关的任务。

每个任务都被定义为来自特定的任务源。 所有来自一个特定任务源的并且指向特定事件循环的任务集(例如,由Document定时器产生的回调,针对在该Document上的鼠标移动触发的事件,为该Document的解析器排队的任务)必须始终添加 到相同类型的任务队列,但来自不同任务源的任务集可能被放置在不同的任务队列中。

例如,用户代理可以有一个用于鼠标和键盘事件的任务队列(用户交互任务源),另一个用于其他任务。 然后,用户代理可以在四分之三的时间内将键盘和鼠标事件优先于其他任务,从而保持界面响应,但不会让其他任务队列挨饿,同时又无法处理来其他任务源的事件。

每个事件循环都有一个当前运行的任务。 最初,这是空的。 同时需要处理处理重入(Reentrancy,是指该函数在自己调用自己的时候,不必担心数据被破坏)。 每个事件循环还有一个执行微任务检查点 (performing a microtask checkpoint)标志位,该标志位最初必须为false。 它用于防止performing a microtask checkpoint算法发生重入调用。

处理模型

只要事件循环存在,那么它就必须持续地执行一下步骤:

  1. 让oldestTask成为事件循环任务队列中最老的任务(如果有的话),在浏览上下文事件循环的情况下,忽略关联文档不完全活动的任务。 用户代理可以选择任何任务队列。 如果没有要选择的任务,则跳转到下面的microtasks步骤。

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

  3. 运行 oldestTask.

  4. 将事件循环当前运行的任务设置为空。

  5. 从 任务队列 ( task queue ) 中移除oldestTask

  6. Microtasks:执行一个微任务检查点。

  7. 更新呈现:如果此事件循环是浏览上下文事件循环(而不是worker事件循环),则运行以下子步骤:

    1. now 设为Performance对象的now()方法返回的值。[HRT\]

    2. docs 做为所讨论的Document对象列表,除了必须满足以下条件外,可以任意排序:

      • 任何通过文档A嵌套的文档B必须列在列表中的A之后。

      • 如果存在两个文档A和B,其浏览上下文都是嵌套浏览上下文,并且它们的浏览上下文容器(browsing context containers)都是同一文档C中的元素,则列表中的A和B的顺序,必须匹配它们各自在C中的浏览上下文容器的顺序.

        在迭代文档的以下步骤中,必须按照在列表中找到的顺序处理每个文档。

    3. 如果有顶级浏览上下文B,用户代理认为此时不会从其渲染更新中受益,则从文档中移除其浏览上下文的顶级浏览上下文位于B中的所有Document对象。

      > 顶层浏览上下文是否会因更新渲染而受益取决于各种因素,例如更新频率。例如,如果浏览器试图达到60Hz的刷新率,那么这些步骤只需要每60秒(约16.7ms)。如果浏览器发现顶级的浏览上下文无法维持此速率,那么它可能会下降到该组文档的更可持续的30Hz,而不是偶尔丢失帧。 (这个规范并没有规定何时更新渲染的任何特定模型。)同样,如果顶级浏览上下文在后台,用户代理可能会决定将该页面放慢到4Hz甚至更低。 > > 浏览器可能会跳过更新渲染的另一个例子:通过交错microtask checkpoints(并且没有,例如动画帧回调交错)的方式,以此确保某些任务一个紧接着一个执行。例如,用户代理可能希望将计时器回调合并在一起,而不需要中间渲染更新。

    4. 如果用户代理认为嵌套的浏览上下文B不会从此时更新其呈现中受益,则从文档中移除其浏览上下文位于B中的所有Document对象。

      > 与顶级浏览上下文一样,各种因素都会影响浏览器是否优化嵌套浏览上下文的更新渲染。 例如,用户代理可能希望花费较少的资源来呈现第三方内容,尤其是当用户目前不可见或资源受限时。 在这种情况下,浏览器可能决定不经常更新此类内容的呈现或从不更新。

    5. 文档中的每个完全活动的文档,执行resize事件(run the resize steps)的时间,现在作为时间戳传入。

    6. 文档中的每个完全活动的文档,执行scroll事件(run the scroll steps)的时间,现在作为时间戳传入。

    7. 文档中的每个完全活动的文档,计算媒体查询和报告变化(evaluate media queries and report changes)的时间,现在作为时间戳传入。

    8. 文档中的每个完全活动的文档,更新动画和相应事件(update animations and send events)的时间,现在作为时间戳传入。

    9. 文档中的每个完全活动的文档,执行全屏事件(run the fullscreen steps)的时间,现在作为时间戳传入。

    10. 文档中的每个完全活动的文档,执行动画帧回调(run the animation frame callbacks)的时间,现在作为时间戳传入。

    11. 文档中的每个完全活动的文档,执行intersection observations(run the update intersection observations steps)的时间,现在作为时间戳传入。

    12. 对于文档中的每个完全活动的Document,更新该Document的呈现或用户界面及其浏览上下文以反映当前状态。

  8. 如果这是一个worker件循环(即为WorkerGlobalScope运行一个循环),但事件循环的任务队列中没有任务并且WorkerGlobalScope对象的关闭标志为true,则销毁事件循环,中止这些步骤,恢复运行 下面的Web Worker部分描述了的步骤。

每个事件循环都有一个microtask队列。microtask任务不同于task任务,microtask最初在microtask队列中而不是task队列。有两种类型微任务:孤立回调microtasks(solitary callback microtasks)和复合microtasks(compound microtasks.)。

此规范仅具有单独的回调微任务。使用复合微任务的规范必须格外小心,以包装回调来处理事件循环。

microtask的排队算法,它必须附加到相关的事件循环的microtask队列;这类microtask的任务源属于microtask任务源。

如果在初始执行过程中它将转换事件循环,则可以将微任务移动到常规任务队列中。在这种情况下,microtask任务源是使用的任务源。通常,微任务的任务源是不相关的。

当一用户代理要执行一次 设置microtask 检查点时,如果该标志位为 false ,那么用户代理会执行一下步骤:

  1. performing a microtask checkpoint标志位设为 true

  2. 此时event loop的 microtask 队列不为空

    1. 将event loop microtask队列中的 oldestMicroTask 作为最旧的 microtask

    2. 设置当前 event loop 正在执行的任务(currently running task) 作为 oldestMicroTask

    3. 执行 oldestMicroTask

      > 这可能涉及调用脚本回调,最终在执行clean up after running script这一步骤中,会再次调用perform a microtask checkpoint算法,这就是为什么我们使用perform a microtask checkpoint标志位来避免重入的原因。

    4. 将event loop 的 currently running task 设置为null

    5. 从 microtask queue中移除 oldestMicrotask。

  3. 对其所主管的事件循环内的每个环境设置对象( environment settings object ),通知有关该环境设置对象rejected promises。

  4. 清理 indexed DB 转换。

  5. performing a microtask checkpoint 标志置为false。

如果在复合微任务运行时,用户代理需要执行复合微任务子任务以运行一系列步骤,则用户代理必须执行以下步骤:

  1. 将事件循环内当前正在运行的任务(the currently runningcompound microtask)作为parent

  2. 将子任务作为新任务,新任务由下面这一系列步骤组成。该 microtask的任务源作为 microtask task source。这是一个合成microtask子任务。

  3. 将event loop内正在运行的任务作为subtask。

  4. 执行子任务。

  5. 将事件循环当前正在运行的任务返回至parent。

当并行运行的算法要等待稳定状态时,用户代理必须对运行以下步骤的microtask进行排队,然后必须停止执行(如下面的步骤中所述,当微任务运行时,将继续执行算法):

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

  2. 如果合适,按照算法步骤中的描述,并行恢复算法的执行。

当算法要求遍历事件循环直到达到条件目标时,用户代理必须执行以下步骤:

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

    > task 作为一个孤立回调microtask时,可以是 microtask。也可能作为复合microtask的子任务或者一个通常的task而不是microtask。不过不会变成一个复合microtask。

  2. 将任务源作为任务的task source

  3. JavaScript execution context stack 复制一份到 old stack。

  4. 清空 JavaScript execution context stack

  5. Perform a microtask checkpoint.

  6. 停止任务,即使算法已在重新调用任务,依然继续执并行执行该步骤。

    > 这将导致以下算法之一继续执行:事件循环的主要步骤集,执行微任务检查点算法,或执行复合微任务子任务算法。

  7. 等待直到满足结果。

  8. 排队任务以使用任务源任务源继续运行这些步骤。等到这个新任务运行后再继续这些步骤。

  9. 用旧堆栈替换JavaScript执行上下文堆栈。

  10. 返回至调用者。

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

  1. 如有必要,更新任何文档或浏览上下文的呈现或用户界面以反映当前状态。

  2. 等到条件目标得到满足。 虽然用户代理具有暂停的任务,但相应的事件循环不得运行更多任务,并且当前正在运行的任务中的任何脚本都必须阻止。 用户代理应该在暂停时保持对用户输入的响应,然而,尽管容量减少,事件循环不会做任何事情。