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 loop 和 document 是可选输入:
- 如果未提供
event loop,将使用隐式事件循环。 - 如果未提供
document,将使用隐式 document 。 - 生成一个新的任务。
- 原 steps 作为新的 steps 。
- 原 source作为新的 source 。
- 原 document 作为新的 document 。
- 将脚本执行环境对象集合置为空集合。
- 找到事件循环中与该任务source关联的任务队列。
- 将任务添加到任务队列。
注意:不传入
event loop和document意味着采用含糊不定的隐式事件循环和隐式 document 。此规范作者建议始终传递这两个值,或者封装一个算法用于全局/ html 元素任务入队。建议封装算法。
想使全局任务按任务源排入任务队列,需提供一个全局对象 ,完成下面一系列操作:
- 使用全局对象相关代理的事件循环。
- 如果全局对象是 window 对象,document 取 window 下的 document ,否则为空。
- 传入 source 、event loop 、document 、steps ,完成前面的任务入队流程。
想使 html 元素任务按任务源排入任务队列,需提供一个 element,完成下面一系列操作:
- 将 global 设为
element所属全局对象。 - 传入 source、global、steps,完成前面的全局任务入队流程。
2.2 微任务排队
想使微任务排入微任务队列需要完成一系列操作,过程中 event loop 和 document 是可选输入:
- 如果未提供
event loop,将生成隐式事件循环。 - 如果未提供
document,将生成隐式document。 - 生成一个新的微任务。
- 原 steps 作为新的 steps 。
- 原 source 作为微任务源。
- 原 document 作为新的 document 。
- 将脚本执行环境对象集合置空。
- 将该微任务排入事件循环的微任务队列。
2.2 隐式事件循环和隐式 document
如果在微任务初始执行时会旋转事件循环,它将可能被移入宏任务队列。这是唯一一种读取微任务的source、document、脚本执行环境对象的集合的情况。微任务执行检查时将忽略对这些字段的读取操做。
任务入队中的隐式事件循环是从任务执行上下文中推导的事件循环。因为多数规范的算法实现此时会关联一个唯一的代理(也就有一个唯一的事件循环),所以这种行为还是可靠的。涉及跨代理通信的算法是一个例外(比如窗口和 worker 之间),这种情况下不能依赖隐式事件循环的概念,规范要求必须在入队时显示提供事件循环。
在任务入队时隐式 document 遵循以下规则:
- 如果不是在窗口事件循环中,返回 null
- 如果任务入队时处于 html 元素上下文中,那么返回该元素所在的 document
- 如果任务入队时处于浏览器上下文中,那么返回浏览器激活的 document
- 如果任务由一个脚本入队,那么返回脚本所属的 document
- 前面四个条件至少命中一个,因此没有第五个
总结
前两小节阐述了:
- 事件循环与用户代理与线程的关系
- 事件循环的分类
- 事件循环、任务队列、微任务队列、任务的结构定义
- 宏任务和微任务入队时的操作步骤 对于微任务执行检查标识、可重入和封装入队算法的意图不太理解 😔 ,希望后面的阅读能够解答。