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

378 阅读13分钟

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

3. 事件处理模型

只要事件循环存在,就会重复执行下面的步骤:

  1. 选择事件循环中的一个任务队列作为 taskQueue ,选择的方式由实现方定义 ,但是选中的任务队列至少要包含一个可执行的任务。如果没有这样的任务队列,会转而执行下面微任务的步骤。

译者注: taskQueue 的选择方式与实现方对用户代理的实现有关。

注意 微任务队列不是任务队列,所以不会在这一步被选中。但是微任务源关联的任务队列可能被选中。这种情况下,下一步选中的任务最初是一个微任务,但是会作为旋转事件循环的一部分被移除。

  1. taskQueue 中的第一个可执行任务将赋值给 oldestTask ,然后这个任务会从 taskQueue 中移除。
  2. 将事件循环的当前正在执行的任务赋给 oldestTask 。
  3. 将当前时间精度赋值给 taskStartTime 。
  4. 执行 oldestTask 中的 steps 。
  5. 将事件循环的当前正在执行的任务再置为 null 。
  6. 微任务:运行微任务执行检查。
  7. 将 hasARenderingOpportunity 置 false 。
  8. 将当前时间精度赋值给 now 。
  9. 执行以下步骤来汇报任务执行时间:
    1. 将顶级浏览上下文 set 置为空集合。
    2. 遍历 oldestTask 的脚本执行环境对象的集合,将每一个环境对象的顶级浏览上下文加入顶级浏览上下文 set 。
    3. 携带 taskStartTime、now(任务的结束时间)、顶级查询上下文 set 、oldestTask,执行长任务汇报 。

    译者注:长任务汇报,执行超过50ms的任务将汇报给用户代理。

  10. 如果在窗口事件循环,更新渲染
    1. 将 docs 置为与当前事件循环关联的代理的所有 document 。在满足下面条件的前提下,可以任意排序:

      • 作为容器的文档 document 要排在其从属前。
      • 有相同容器的文档,排序的顺序与他们在容器中的顺序一致。 在下面遍历docs的过程中,处理document的顺序要与排序一致。
    2. 渲染机会(rendering opportunity):删除docs中所有不显示的document。

      如果用户代理能为用户展示该浏览上下文的内容,该浏览上下文就有渲染机会。这里会考虑到硬件刷新率和用户代理的性能限制。需要注意的是即时内容出现在视口外也是可以渲染的。

      浏览上下文的渲染机会是由硬件因素(比如屏幕刷新率)和其他因素(比如页面性能和页面是否在后台)确定。渲染机会通常会定期出现。

      本规范不强制用任何特定的模式选择渲染机会。但是比如浏览器想要达到60Hz刷新率,那么渲染机会最慢也要一秒出现60次(大约16.7毫秒)。如果浏览器发现某个浏览上下文无法维持这样的刷新率,则该浏览上下文的渲染机会可能会被降到每秒30次,避免间歇的丢帧。与此类似,如果浏览上下文不可见,用户代理可能将它的渲染机会给予频率降低到每秒4次,或者更低。

    3. 如果 docs 非空,将 hasARenderingOpportunity 置 true 。

    4. 渲染冗余:从 docs 中删除满足以下两个条件的所有 document 对象:

      • 用户代理认为更新这个浏览上下文不会有明显的效果。
      • document 的动画帧回调映射是空的。
    5. 从 docs 中删除由于其他原因使得用户代理认为最好跳过渲染更新的 document 。

      『渲染机会』那一步可以防止用户代理在无法向用户渲染新内容(没有渲染机会)时更新渲染。『渲染冗余』那一步可以保证用户代理在没有新的要绘制的内容时更新渲染。而当前这一步防止 steps 意外执行,比如某些任务需要在其他任务后立即执行,确保此时可以插入微任务执行检查(不插入动画帧回调)。具体上说,用户代理可能希望 timer 回调合并在一起,中间不要插入渲染更新。

    6. 对于 docs 中的每个完全激活的 document ,会迅速将浏览上下文是顶级上下文的 document 作为自动聚焦候选。

    7. 对于 docs 中的每个完全激活的 document ,在运行 document 的 resize 操作时,会将 now 以时间戳传入。

    8. 对于 docs 中的每个完全激活的 document ,在运行 document 的滚动操作时,会将 now 以时间戳传入。

    9. 对于 docs 中的每个完全激活的 document ,在评估媒体查询并汇报变化时,会将 now 以时间戳传入。

    10. 对于 docs 中的每个完全激活的 document ,在更新动画并触发事件时,会将 now 以时间戳传入。

    11. 对于 docs 中的每个完全激活的 document ,在运行 document 的全屏操作时,会将 now 以时间戳传入。

    12. 对于 docs 中的每个完全激活的 document ,如果用户代理检测到与 CanvasRenderingContext2D 或 OffscreenCaasRenderingContext2D 关联的上下文已经丢失,需要为此类情况执行上下文丢失步骤:

      1. 如果上下文是 CanvasRenderingContext2D ,则上下文的 canvas 属性取值 canvas ,否则取值上下文的关联 OffscreenCanvas 对象。
      2. 上下文的 context lost 置 true 。
      3. 将渲染上下文重置为上下文提供的默认值。
      4. canvas 上触发名为 contextlost 的事件,携带一个默认值 true ,事件结果将赋值给 shouldRestore 。
      5. shouldRestore 如果是 false ,终止后面几步。
      6. 尝试创建上下文属性的备份并将其与上下文关联从而还原上下文。如果失败,则中止后面几步。
      7. 上下文的 context lost 置 false 。
      8. canvas 上触发一个 contextrestored 事件。
    13. 对于 docs 中的每个完全激活的document,执行该 document 的动画帧回调,会将 now 以时间戳传入。

    14. 对于 docs 中的每个完全激活的 document ,执行该 document 的更新重叠关系检查,会将 now 以时间戳传入。

    15. 为 docs 中的每个 Document 对象调用渲染时间点标记算法。

    16. 对于 docs 中的每个完全激活的document,更新该 document 的渲染或用户界面及其浏览上下文来反映当前状态。

  11. 如果下面的条件都成立:
    1. 是窗口事件循环
    2. 该事件循环的任务队列中没有 document 为激活状态的任务
    3. 事件循环的微任务队列为空
    4. hasARenderingOpportunity 是 false 那么:
    5. computeDeadline 做下面几步:
      1. 将事件循环的上一次空闲开始时间加50赋值给 deadline

      50ms 的限制是保证页面对未来新的用户动作的响应可以被人类感知接受。 译者注:50ms 即每秒20帧更新,人眼看起来可动。

      1. hasPendingRenders 置 false
      2. 遍历该事件循环的同循环窗口,赋值给 windowInSameLoop
        1. 如果 windowInSameLoop 的动画帧回调映射非空,或者用户代理认为 windowInSameLoop 可能阻塞渲染,hasPendingRenders 将置 false
        2. 将 windowInSameLoop 的响应时间映射表的值集合赋值给 timerCallbackEstimates
        3. 遍历 timerCallbackEstimates ,如果某个的 timeoutDeadline 小于 deadline ,更新 deadline 保持 deadline 最小。
      3. 如果 hasPendingRenders 是 true ,那么:
        1. 将该事件循环上一次渲染机会时间增加一段时间(1000 / 当前刷新率)赋值给 nextRenderDeadline 。 刷新率可以指硬件也可以指软件实现。对于60Hz刷新率, nextRenderDeadline 将在上次渲染机会时间后加约16.67ms。
        2. 如果 nextRenderDeadline 比 deadline 小,返回 nextRenderDeadline 。
      4. 返回 deadline 。
    6. 遍历同循环窗口,带入 computeDeadline 为每个 window 对象执行空闲时间算法。
  12. 如果这是一个 worker 事件循环,那么:
    1. 如果该事件循环的代理的作用域全局对象满足 DedicatedWorkerGlobalScope 而且用户代理认为此时更新渲染比较好,那么:
      1. 将当前时间精度赋值给 now 。
      2. 执行 DedicatedWorkerGlobalScope 的动画帧回调,将 now 以时间戳传入。
      3. 更新这个专用 worker 的渲染,以反映当前的状态。

      与窗口事件循环中对更新渲染的阐述类似,用户代理可以控制专用 worker 的更新频率。

    2. 如果该事件循环的任务队列没有任务,而且 WorkerGlobalScope 对象的 closing 为 true,就要销毁事件循环,终止后面的步骤,执行 Web Workers 一章中 初始化 worker 的步骤。

当用户代理要做微任务执行检查

  1. 如果该事件循环的微任务执行检查标识是 true ,直接返回。
  2. 将该事件循环的微任务执行检查标识置为 true 。
  3. 在该事件循环的微任务队列非空前提下,循环下面操作:
    1. 将该事件循环的微任务队列推出队首赋值给 oldestMicrotask 。
    2. 将 oldestMicrotask 作为事件循环的当前正在执行的任务。
    3. 执行 oldestMicrotask 。

    期间可能会执行用户脚本的回调,执行过后会清除,然后会再次调用微任务执行检查算法,这就是我们需要微任务执行检查标识来避免重入的原因。

    1. 再将当前执行的任务置回 null 。
  4. 处理环境设置对象所负责的事件循环,通知环境设置对象上所有reject的promise
  5. 运行事件循环的【清理任务】
  6. 运行 ClearKeptObjects

WeakRef.prototype.deref()返回的对象会一直保持活性,直到后面某次ClearKeptObjects中,对象被垃圾回收。

  1. 将该事件循环的微任务执行检查标识置 false

当有一个异步算法等待返回值时,用户代理会将一个微任务入队,执行以下步骤,算法暂停执行(微任务执行时,算法将恢复执行,如下所述:)

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

2. 按算法的实现,在合适的时机恢复异步算法执行。

旋转事件循环直到满足条件的过程等同于以下步骤:

  1. 将当前事件循环的当前正在执行的任务作为 task

task也可能是微任务

  1. 将task的源作为source
  2. 将js执行栈的副本作为oldStack
  3. 清空js执行栈
  4. 执行微任务执行检查
  5. 异步执行:
    1. 等待条件满足
    2. 入队一个完成如下操作的微任务:
      1. 将oldStack作为js执行栈
      2. 旋转事件循环,然后执行原始算法后面的语句

      这里恢复了task的执行

  6. 暂停 task 直到调用它的算法将其恢复

这将促使事件循环的核心步骤和微任务执行检查继续执行

本规范旋转事件循环的方式和其他规范的其他算法(类似编程语言的函数调用)不同,本规范的实现更像是一个宏,通过混入一些步骤和操作,节省一些上层的码量和缩进。

例如:

某一个算法完成下面的工作:

  1. 做一些工作
  2. 旋转事件循环,直到一些条件达成
  3. 做其他事情 上面是简化版,经过宏处理,详细过程如下:
  4. 做一些工作
  5. 将js执行栈的副本作为oldStack
  6. 清空js执行栈
  7. 执行微任务执行检查
  8. 异步执行:
    1. 等到一些条件达成
    2. 入队一个完成如下操作的任务:
      1. 将oldStack作为js执行栈
      2. 做其他事情

例如:

下面是一个更完整的示例,其中事件循环依靠向异步队列推入任务旋转。使用旋转事件循环的版本:

  1. 异步执行:
    1. 执行异步工作1
    2. 入队一个DOM任务源的任务:
      1. 执行任务1
      2. 旋转事件循环,直到一些条件达成
      3. 执行任务2
    3. 执行异步工作2 详细过程如下:
  2. 异步执行:
    1. 执行异步工作1
    2. 把 oldStack 置 null
    3. 入队一个DOM任务源的任务:
      1. 执行任务1
      2. 将js执行栈的副本作为oldStack
      3. 清空js执行栈
      4. 执行微任务执行检查
    4. 等到一些条件达成
    5. 入队一个完成如下操作的任务:
      1. 将oldStack作为js执行栈
      2. 执行任务2
    6. 执行异步工作2 由于历史原因,本规范中的一些算法要求用户代理在执行任务时暂停,直到满足条件。这里的意思是:
  1. 将当前状态更新出去,反映到任何与当前状态相关的文档、浏览上下文的呈现以及用户界面。
  2. 等待满足条件。当用户代理有一个暂停的任务时,相应的事件循环不能再运行其他任务,并且当前运行的任务中的任何脚本执行都会中断。然而,用户代理应该在暂停时保持对用户输入的响应,尽管不能完成什么,因为事件循环此时不处理任务。

注意 暂停对用户体验非常有害,尤其是在多个文档共享单个事件循环的情况下。规范鼓励用户代理尝试暂停的替代方案,比如:旋转事件循环,即使是不执行任何任务,简单的重复旋转,只要有可能做到这一点,同时保持与现有内容的兼容性。如果发现了不那么激进的web兼容的替代方案,那么规范将很乐意调整。

在此期间,实现者应该意识到,用户代理可能尝试的各种替代方案,事件循环的行为可能有微妙的改变,包括任务和微任务计时。虽然依此实现会违反暂停操作真实的语义,也应该继续。

总结

第三小节阐述了:

  • 事件循环处理模型
  • 事件循环旋转的实现
  • 解释了微任务执行检查的操作 这里是理解事件循环的核心内容,规范的叙述像代码实现的描述。也许可以用js实现一些伪代码帮助理解。