【译】HTML标准-8.1.6事件循环(一)

341 阅读8分钟

HTML 规范篇幅极长,阐述了很多前端技术的底层细节。我没有雄心通篇翻译,只打算挑部分感兴趣的内容。译文中如果存在表达不恰当的内容欢迎提出您宝贵的意见。

1. 定义

为了调度 事件、用户交互、脚本、渲染、网络等任务,用户代理需要用到事件循环。每个代理与事件循环一一对应。

译者注:用户代理,浏览器对外界交互的代理层。分为同源窗口代理、专用 worker 、共享 worker 、 Service worker 代理、以及 worklet 代理。简单理解为,不使用 worker 和 worklet 时,都是同源窗口代理。 同源窗口的事件循环叫窗口事件循环 。专用 worker 、共享 worker 、 service worker 的事件循环叫 worker事件循环。此外还有 worklet 事件循环。

译者注:专用 worker ,与共享worker、Service Worker同属于 Web Workers 。大众共识的 Web Worker 多有『专用』的意思。

一个事件循环不一定对应一个线程实现。比如多个窗口事件循环可以在同一个线程内调度。但是对于 [[CanBlock]] 为 true 的各种 worker 代理,JS规范明确要求了 forward progress ,这也就是要求每个代理有自己的线程。

事件循环有一个或多个任务队列。任务队列是一个由任务组成的有序集合。

译者注:后文『任务队列』都指与微任务队列相对的宏任务队列,二者结构不同。

任务队列是有序集合不是队列。因为事件循环处理模型的第一步是从选中『队列』中找到第一个可以运行的任务,而不是推出第一个任务。

微任务队列不是任务队列。

1.1 任务定义

『任务』一词涵盖了负责以下工作的程序:

  • 事件:使用 EventTarget 调度的事件通常是在一个专用的任务中完成。不是所有事件都由任务队列调度,很多是在其他任务执行过程中触发。
  • 解析: HTML 解析器解析一或多个字符转化成一些标记产出,通常是一个任务。
  • 回调:调用一个回调通常是在一个专用的任务中完成。
  • 使用资源:程序经常需要获取资源。如果使用非阻塞的方式获取资源,那么在有部分资源可使用后对资源的处理需要一个任务。
  • 响应DOM操作:一些 html 元素会拥有一些由DOM操作触发的任务。比如,一个插入文档中的 html 元素。

形式上,任务是由以下内容构成的结构:

  • steps:完成任务所经过的一系列具体工作。
  • source:众多任务来源之一,用来将相关任务分组和关联。
  • document:当前任务相关的 document 。非窗口事件循环的任务这里取 null 。
  • 脚本执行环境对象的集合: 一个用来在任务期间跟踪脚本执行环境对象的集合。

译者注:脚本执行环境对象的集合包括但不限于任务执行上下文。

只有在 document 是 null 或完全激活状态时,这个任务才是可执行的。至于 source 字段,每个任务都来自一个特定的任务源。每个事件循环中的每个任务源必须关联特定的任务队列。

本质上,source在标准中用于区分那些不同逻辑类型的任务,用户代理也希望以此区分。任务队列是给用户代理用来将某个事件循环的任务按任务源归类的。

例如,用户代理可以有一个管理鼠标和键盘事件的任务队列(对应用户交互任务源),以及另一个关联其他所有任务源的任务队列。然后利用事件处理模型在初始化阶段的自由度,鼠标和键盘事件将会被优先处理,拥有比其他任务多3/4的时间。这样可以在不延误其他任务的前提下,保持页面响应。需要注意,即使如此配置,处理模型仍然强制用户代理不能处理任何未经规划的任务,无论来自任何任务源。

1.2 事件循环结构定义

  • 每个事件循环都有一个当前正在执行的任务,取值为 null|Task ,用来处理重入的情况。
  • 每个事件循环都有一个微任务队列,是一个微任务组成的队列,初始为空。微任务是由入队微任务算法创建的任务的统称。
  • 每个事件循环都有一个微任务执行检查标识是布尔值,初始为false。它用于阻止重入时调用微任务执行检查算法。
  • 每个窗口事件循环都有一个 DOMHighResTimeStamp ,上一次渲染触发的时间戳,初始为0。
  • 每个窗口事件循环还有一个 DOMHighResTimeStamp ,上一次空闲的开始的时间戳,初始为0。 想要获取获取同循环窗口对于窗口事件循环就是,返回所有相关代理上的 Window 对象。

2. 任务排队

2.1 宏任务排队

想使任务按任务源排入任务队列需要完成一系列操作,过程中 event loopdocument 是可选输入:

  1. 如果未提供 event loop ,将使用隐式事件循环。
  2. 如果未提供 document ,将使用隐式 document 。
  3. 生成一个新的任务。
  4. 原 steps 作为新的 steps 。
  5. 原 source作为新的 source 。
  6. 原 document 作为新的 document 。
  7. 将脚本执行环境对象集合置为空集合。
  8. 找到事件循环中与该任务source关联的任务队列。
  9. 将任务添加到任务队列。

注意:不传入 event loopdocument 意味着采用含糊不定的隐式事件循环和隐式 document 。此规范作者建议始终传递这两个值,或者封装一个算法用于全局/ html 元素任务入队。建议封装算法。

想使全局任务按任务源排入任务队列,需提供一个全局对象 ,完成下面一系列操作:

  1. 使用全局对象相关代理的事件循环。
  2. 如果全局对象是 window 对象,document 取 window 下的 document ,否则为空。
  3. 传入 source 、event loop 、document 、steps ,完成前面的任务入队流程。

想使 html 元素任务按任务源排入任务队列,需提供一个 element,完成下面一系列操作:

  1. 将 global 设为 element 所属全局对象。
  2. 传入 source、global、steps,完成前面的全局任务入队流程。

2.2 微任务排队

想使微任务排入微任务队列需要完成一系列操作,过程中 event loopdocument 是可选输入:

  1. 如果未提供 event loop ,将生成隐式事件循环。
  2. 如果未提供 document ,将生成隐式document。
  3. 生成一个新的微任务。
  4. 原 steps 作为新的 steps 。
  5. 原 source 作为微任务源
  6. 原 document 作为新的 document 。
  7. 将脚本执行环境对象集合置空。
  8. 将该微任务排入事件循环的微任务队列。

2.2 隐式事件循环和隐式 document

如果在微任务初始执行时会旋转事件循环,它将可能被移入宏任务队列。这是唯一一种读取微任务的source、document、脚本执行环境对象的集合的情况。微任务执行检查时将忽略对这些字段的读取操做。

任务入队中的隐式事件循环是从任务执行上下文中推导的事件循环。因为多数规范的算法实现此时会关联一个唯一的代理(也就有一个唯一的事件循环),所以这种行为还是可靠的。涉及跨代理通信的算法是一个例外(比如窗口和 worker 之间),这种情况下不能依赖隐式事件循环的概念,规范要求必须在入队时显示提供事件循环。

在任务入队时隐式 document 遵循以下规则:

  1. 如果不是在窗口事件循环中,返回 null
  2. 如果任务入队时处于 html 元素上下文中,那么返回该元素所在的 document
  3. 如果任务入队时处于浏览器上下文中,那么返回浏览器激活的 document
  4. 如果任务由一个脚本入队,那么返回脚本所属的 document
  5. 前面四个条件至少命中一个,因此没有第五个

总结

前两小节阐述了:

  • 事件循环与用户代理与线程的关系
  • 事件循环的分类
  • 事件循环、任务队列、微任务队列、任务的结构定义
  • 宏任务和微任务入队时的操作步骤 对于微任务执行检查标识、可重入和封装入队算法的意图不太理解 😔 ,希望后面的阅读能够解答。