从规范理解Event Loop

Posted Eval Studio

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从规范理解Event Loop相关的知识,希望对你有一定的参考价值。

规范在哪里?

event loop定义在html规范中。目前HTML规范主要由WHATWG小组主导维护。然而HTML规范曾经长期由两个小组WHATWG和W3C分别制定,同时存在两个规范。


W3C是经典的万维网联盟,是万维网的主要国际标准组织,制定了HTML、DOM、CSS众多规范。

WHATWG由 Opera、Mozilla、Chrome、Safari 一众浏览器厂商04年组建,起因是由于 W3C 开发更现代的 HTML 标准的速度缓慢,并且计划将 HTML 转换成一个名为 XHTML 的变体,浏览器厂商并不同意。WHATWG制定的H5标准被W3批准为下一代HTML标准。


2019年5月份双方才达成协议,共同维护一份HTML标准,其中HTML和DOM由WHATWG维护。也有文章指出,这代表着WHATWG击败了W3C,赢得对HTML和DOM的规范制定权,所以本文则会以WHATWG为准。


eventloop的定义

从规范中找到定义相关章节:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section.

为了协调事件、用户交互、脚本、渲染、网络等,用户代理必须使用本节描述的事件循环。可以看出,event loop的意义就是为了在单线程的js引擎机制下,实现多任务的优先级和异步处理。


event loop都包含什么?

An event loop has one or more task queues. A task queue is a set of tasks.

Each event loop has a currently running task, which is either a task or null. Initially, this is null. It is used to handle reentrancy.

Each event loop has a microtask queue, which is a queue of microtasks, initially empty. A microtask is a colloquial way of referring to a task that was created via the queue a microtask algorithm.

Each event loop has a performing a microtask checkpoint boolean, which is initially false. It is used to prevent reentrant invocation of the perform a microtask checkpoint algorithm.

  • 一个或多个宏任务队列,一个微任务队列。

  • 一个正在执行的task,这个task可以是宏任务,也可以是微任务。

  • 一个bool类型的值(performing a microtask checkpoint),表示微任务队列是否正在执行。规范中指出,微任务执行过程中,可能会唤起新的一轮perform a microtask checkpoint算法,使用这个值来防止重复执行,比如在微任务中trigger一个事件。如下引用描述:

These algorithms are not invoked by one script directly calling another, but they can be invoked reentrantly in an indirect manner, e.g. if a script dispatches an event which has event listeners registered.

注:规范中并没有宏任务(macro task)这样的字眼,但比较容易看出,宏任务队列和event loop中的task queues是一个概念,微任务队列和microtask queue是一个概念。


宏任务

宏任务是个Task的实例,每个宏任务都有确定的任务源(task source),每个任务源都有对应的任务队列。调整队列的执行顺序,便可以控制每个任务源的执行优先级。比如优先执行鼠标与键盘交互任务源的队列,则可以更快的响应用户交互,提供更好的体验。


任务源类型

  • DOM 操作任务源:如元素以非阻塞方式插入文档

This task source is used for features that react to DOM manipulations, such as things that happen in a non-blocking fashion when an element is inserted into the document.

  • 用户交互任务源:用户输入事件(如 click) 必须使用 task 队列

This task source is used for features that react to user interaction, for example keyboard or mouse input.

Events sent in response to user input (e.g. click events) must be fired using tasks queued with the user interaction task source. [UIEVENTS]

  • 网络任务源:网络活动触发的任务。比如XHR

This task source is used for features that trigger in response to network activity.

  • history 回溯任务源:使用 history.back() 或者类似 API

This task source is used to queue calls to history.back() and similar APIs.

除此之外,如果继续在规范里搜  task source   ,还会得到更多的task source,比如 timer task source, websocket task source等。


微任务

微任务也是个Task实例,与宏任务有着相同的构造。但是所处的队列不一样,微任务一般只出现在微任务队列(microtask queue)中。在一些情况下(spin the event loop

),微任务甚至可以拿来当宏任务用,这个时候使用的任务源是 microtask task source 。规范中没有规定微任务源,一般来说,微任务来自于:

  • Promise.then

  • MutationObserver

  • Object.observe


event loop执行流程

下面是规范节选,只提取了关键选段

  1. Let taskQueue be one of the event loop's task queues, chosen in an implementation-defined manner, with the constraint that the chosen task queue must contain at least one runnabletask. If there is no such task queue, then jump to the microtasks step below.

  2. Let oldestTask be the first runnabletask in taskQueue, and remove it from taskQueue.

  1. Set the event loop's currently running task to oldestTask.

  2. Perform oldestTask's steps.

  1. Set the event loop's currently running task back to null.

注:从宏任务队列中取一个宏任务并执行

  1. Microtasks: Perform a microtask checkpoint.

注:执行微任务队列检查,就是平时说的清空微任务队列

  1. Update the rendering: if this is a window event loop, then:

... 删减部分Rendering opportunities获取逻辑,基本是由浏览器判断是否应该渲染,根据是否在后台、刷新率和硬件性能等,属于浏览器自主行为。如果最后没有刷新机会,那么docs就是空的,下面几段就不会执行。

  1. For each fully activeDocument in docs, flush autofocus candidates for that Document if its browsing context is a top-level browsing context.

  2. For each fully activeDocument in docs, run the resize steps for that Document, passing in now as the timestamp. [CSSOMVIEW]

  3. For each fully activeDocument in docs, run the scroll steps for that Document, passing in now as the timestamp. [CSSOMVIEW]

  4. For each fully activeDocument in docs, evaluate media queries and report changes for that Document, passing in now as the timestamp. [CSSOMVIEW]

  5. For each fully activeDocument in docs, update animations and send events for that Document, passing in now as the timestamp. [WEBANIMATIONS]

  6. For each fully activeDocument in docs, run the fullscreen steps for that Document, passing in now as the timestamp. [FULLSCREEN]

  7. For each fully activeDocument in docs, run the animation frame callbacks for that Document, passing in now as the timestamp.

  8. For each fully activeDocument in docs, run the update intersection observations steps for that Document, passing in now as the timestamp. [INTERSECTIONOBSERVER]

  9. Invoke the mark paint timing algorithm for each Document object in docs.

  10. For each fully activeDocument in docs, update the rendering or user interface of that Document and its browsing context to reflect the current state.

注:这个阶段开始触发resize、scroll、requestAnimationFrame回调。这里真正执行requestAnimationFrame的时候,并没有构建Task实例,也没有对应的task source,所以不应该算宏任务。如果存在多个requestAnimationFrame回调,也是会在这一步完成。

  1. If all of the following are true

  • this is a window event loop

  • there is no task in this event loop's task queues whose document is fully active

  • this event loop's microtask queue is empty

  1. then for each Window object whose relevant agent's event loop is this event loop, run the start an idle period algorithm, passing the Window. [REQUESTIDLECALLBACK]

注:执行requestIdleCallback回调

  1. If this is a worker event loop, then:

  1. If this event loop's agent's single realm's global object is a supportedDedicatedWorkerGlobalScopeand the user agent believes that it would benefit from having its rendering updated at this time, then:

  1. Let now be the current high resolution time. [HRT]

  2. Run the animation frame callbacks for that DedicatedWorkerGlobalScope, passing in now as the timestamp.

  3. Update the rendering of that dedicated worker to reflect the current state.

注:对于专用worker event loop,执行request animation 回调。


借用网上常用的event loop的图表示整个流程如下:

仍然存在问题?

到目前为止,event loop的执行流程是:每次执行一个宏任务,并清空微任务。但是解释不了下面的案例:

setTimeout(() => {
console.log('run macro')
})
Promise.resolve().then(() => {
console.log('run micro')
});
// 输出:run micro
// 输出:run macro

每次先取一个宏任务执行,那就应该先输出 run macro 才对,但事实却不是这样。这就得从微任务的执行时机说起。经过对规范的阅读,发现微任务会在以下时机触发:


  • 解析执行完脚本后执行微任务。可以解释上面的案例存在的问题。解析执行完脚本,立即清空微任务队列,下一轮event loop执行setTimeout函数。

Calling scripts -> clean up after running script -> Perform a microtask checkpoint

  • 每轮evenloop执行过宏任务后执行微任务,见event loop执行流程的第7步。

Perform a microtask checkpoint 尝试执行微任务队列里的所有微任务

  • 触发事件后执行微任务,可以是js模拟派发,可以是用户点击。

call a user object’s operation -> clean up after running script -> Perform a microtask checkpoint

  • requestAnimationFrame执行完回调后执行微任务。

run the animation frame callbacks -> Invoking callback functions -> clean up after running script -> Perform a microtask checkpoint

  • 微任务进入队列的时候

  • ...

上面几个微任务执行点,基本覆盖了微任务的大部分执行点,也是大部分js的执行时机,js执行要么就是初始化执行的时候,要么就是期间的各种事件回调。执行完脚本后,会立即尝试清空一次微任务(如果执行栈是空的,就立即执行并清理),所以这个机制也可以说是只要执行栈为空,就会执行微任务队列。按理说,只保留event loop的那一个执行点,就能保证每个微任务得到执行。不这么处理可能是把脚本执行和微任务分开可能会有问题,设想初始化一段脚本,但是把一些绘制相关的放在了微任务里,如果微任务下次event loop才执行,那么网页就有可能会白屏一帧。所以一轮eventloop里会多次执行微任务,而不是常说的一次。


再举个例子体验一次宏任务,微任务多点执行:

document.addEventListener('click', () => {
console.log('macro')
queueMicrotask(() => console.log('micro'))
});
document.addEventListener('click', () => {
console.log('macro')
queueMicrotask(() => console.log('micro'))
});
// 点击 -> 触发一次宏任务 -> 派发click事件 -> 执行捕获和冒泡 -> 执行两个click回调
// -> 然后输出啥?是macro,macro,micro,micro吗?
// -> 实际输出:macro,micro,macro,micro


总结

对规范的阅读和各路文章整理,又对event loop了解多了一些东西

  • event loop 执行流程是每轮单个宏任务+多次清空微任务队列(正常流程里的一次、requestAnimationFrame回调、resize事件等)。

  • requestAnimationFrame 不是宏任务,因为没有对应的task source,至少规范是这样的。

  • 清空微任务队列和js执行栈是否为空有很大关系,虽然有很多执行机会,但是执行栈不为空是不执行的。

  • 其他知识点:setTimeout/setInterval定义在html规范里;queueMicrotask可以用来插入微任务,不用写个Promise.resolve。


以上是关于从规范理解Event Loop的主要内容,如果未能解决你的问题,请参考以下文章

[未解决问题记录]python asyncio+aiohttp出现Exception ignored:RuntimeError('Event loop is closed')(代码片段

一段代码理解浏览器和服务端中的Event Loop

浏览器的 Event Loop

理解 Node.js 的 Event loop

全方位理解JavaScript的Event Loop

如何理解JS中的事件循环(Event Loop)