Event Loop:事件循环

来自Wikioe
Eijux讨论 | 贡献2023年3月24日 (五) 06:58的版本
跳到导航 跳到搜索


关于

在 MDN 上看到 JavaScript 的《并发模型与事件循环》一节,但由于该页面内容过少,实在不变学习。

而网络上搜到的内容大同小异,互相又有不小出入,难以拿捏。

众所周知,JavaScript 是没有所谓源代码的“源代码”,所以这部分内容只能在“规范”文件[1]了。
  • 以下内容参照《HTML Standard》、MDN、与网络内容整理。

前置知识

见:浏览器基础:进程与线程

“一个 Tab 页面对应一个渲染进程”,而“渲染进程中只能存在一个 JS 引擎线程”(即,主线程),即:页面中的所有 JS 都由同一单线程运行。

??????由于“渲染进程”负责了页面几乎所有的任务(事件、交互、脚本、渲染、网络),所以如何协调各个任务的执行是关键问题。

浏览器的进程与线程.png

JavaScript的“任务”

JavaScript中,“任务”被分为两种:“同步任务”,“异步任务”。“异步任务”(Task):又分为“宏任务”(MacroTask)与“微任务”(MicroTask)。
  1. 同步任务:在“主线程”上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
  2. 异步任务:不直接进入“主线程”,而进入“任务队列”(task queue)的任务。
    1. 宏任务(macrotask)
    2. 微任务(microtask)



JavaScript的“运行时”

与 Java 的运行时概念类似,JavaScript 也有其运行时。



Event Loop

要协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理必须使用本节中描述的事件循环。

每个“代理”(Agent[2])有一个关联的“事件循环”,且该循环对该代理是唯一的。

  1. Similar-origin window agent 的事件循环称为“窗口事件循环”(window event loop)。
  2. Dedicated worker agent、Shared worker agent 或 Service worker agent 的事件循环称为“工作事件循环”(worker event loop)。
  3. Worklet agent 的事件循环称为“工件事件循环”(worklet event loop)。
  • 事件循环不一定对应于实现线程。(例如,可以在单个线程中协同调度多个窗口事件循环)


  • 一个事件循环具有一个或多个“任务队列”(task queues)
    • 任务队列是“集合”(set),而不是“队列”(queue),因为事件循环处理模型从所选队列中获取第一个可运行的任务,而不是将第一个任务出列。
    • 微任务队列(microtask queue)不是任务队列(task queues)
  • 每个事件循环都有一个当前“正在运行的任务”(currently running task),要么是一个任务,要么是 null。初始为空。
    • 它用于处理可重入性。
  • 每个事件循环都有一个“微任务队列”(microtask queue),该队列是一个初始为空的微任务队列。
  • 每个事件循环都有一个“正在执行微任务的检查点”(performing a microtask checkpoint)的布尔值。该值最初为 false。
    • 它用于防止重入调用。
  • 每个窗口事件循环都有一个 DOMHighResTimeStamp 表示“上次渲染机会时间”(last render opportunity time),最初设置为零。
  • 每个窗口事件循环都有一个 DOMHighResTimeStamp 表示“上一个空闲周期的开始时间”(last idle period start time),最初设置为零。

任务(Tasks)

“任务”(Tasks)包括以下几种:

  1. 事件(Events):在特定 EventTarget 对象上调度 Event 对象,通常由专用的任务完成。
  2. 解析(Parsing):HTML 解析器标记一个或多个字节,然后处理任何生成的标记,通常是一项任务。
  3. 回调(Callbacks):调用回调通常由一个专用的任务完成。
  4. 使用资源(Using a resource):当算法获取资源时,如果获取是以非阻塞的方式发生的,那么一旦部分或全部资源可用,就由任务执行对资源的处理。
  5. 对 DOM 操作做出反应(Reacting to DOM manipulation):一些元素具有响应DOM操作而触发的任务,例如:当该元素被插入到文档中时。


从形式上讲,“任务”(Tasks)具有以下结构:

  1. 步骤(steps):指定任务要完成的工作的一系列步骤。
  2. 来源(source):用于对相关任务进行分组和序列化。
  3. 文档(document)与任务关联的文档,对于不在“窗口事件循环”中的任务则为 null。
  4. 脚本评估环境设置对象集(A script evaluation environment settings object set):用于跟踪任务期间的脚本评估。

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

处理模型

只要事件循环存在,它就必须持续运行以下步骤:

  1. 让 oldestTask 和 taskStartTime 为 null。
  2. 如果事件循环有一个任务队列其中至少有一个可运行的任务,则:
    1. 由实现(浏览器)定义的规则,选择一个任务队列作为 taskQueue。(“微任务队列”并不是一个“任务队列”,所以不会被选择)
    2. 将 taskStartTime 设置为“不安全的共享当前时间”。
    3. 将 oldestTask 设置为 taskQueue 中的第一个可运行任务,并将其从 taskQueue 中删除。——【任务队列是“set”,所以并非“出队”】
    4. 将事件循环的“正在运行的任务”设置为 oldestTask。
    5. 执行 oldestTask 的步骤(Steps)。
    6. 将事件循环的“正在运行的任务”设置回 null。
  3. 执行微任务检查点
    1. 如果事件循环的“正在执行微任务的检查点”为 true,则返回。——【检查重入】
    2. 将事件循环的“正在执行微任务的检查点”设置为 true。——【防止重入】
    3. 当事件循环的“微任务队列”不为空时:——【!!!不为空将一直重复执行!!!】
      1. 从事件循环的“微任务队列”中“出队”一个任务作为 oldestMicrotask。
      2. 将事件循环的“正在运行的任务”设置为 oldestMicrotask。
      3. 执行 oldestMicrotask。
        • 这可能涉及调用脚本的回调,并最终调用“clean up after running script”步骤,而该步骤的最后一步又将调用执行微任务检查点。这就是为什么设置“正在执行微任务的检查点”来防止“重入”。
      4. 将事件循环的“正在运行的任务”设置回null。
    4. 对于负责的事件循环为该事件循环的每个“环境设置对象”,通知该“环境设置对象”上 rejected 的 promise。
    5. 清理索引数据库事务。
    6. 执行 ClearKeptObjects()。
    7. 将事件循环的“正在执行微任务的检查点”设置为false。
  4. 将 hasARenderingOpportunity 设置为 false。
  5. 将现在时刻设置为“不安全的共享当前时间”。
  6. 如果 oldestTask 不为 null,则:
    1. ……(省略)
  7. 更新渲染:如果这是一个窗口事件循环,则:
    1. ……(省略)
  8. ……(省略)

总结

当宏任务和微任务都处于 任务队列(Task Queue) 中时,微任务的优先级大于宏任务,即先将微任务执行完,再执行宏任务;
这是理解上优点小偏差


补充内容

如何使用“微任务”(microtask)?

如何实现无延迟的 setTimeout?

参考

  1. 参见:《HTML Standard》:“Event loops”一节
  2. “代理”(Agent)包括一组:ECMAScript执行上下文、执行上下文堆栈、运行的执行上下文、代理记录和执行线程。除了执行线程之外,代理的组成部分只属于该代理。

    从概念上讲,代理概念是一个独立于体系结构、理想化的“线程”,JavaScript代码在其中运行。

    • 一个宿主(浏览器、Node.js)可以有多个 Agent,当两个 Agent 同处一个“Agent Clusters”(代理集群)时,它们可以共享内存。
    • Agent 之间相互隔离,可以通过发送消息进行通信。
    • 一个 Agent 对应一条“执行线程”。
    • 每个 ECMAScript 程序都必须在一个名为 Agent 的区域内执行。

    参考:

    1. 《HTML Standard》:“Agents and agent clusters”一节
    2. 《ECMAScript® 2023 Language Specification》:“Agents”一节
    3. 【人人都能读标准】8.可视化JavaScript的运行环境:agents、执行上下文、Realm