# event loop
通过HTML5
规范 (opens new window)的定义来看event loop
的定义
为了协调时间,用户交互,脚本,界面渲染,网络等等,用户代理必须使用下一节描述的 event loops。event loops 分为两种:浏览器环境及为 Web Worker 服务的。
在这里只关注浏览器部分,JavaScript
引擎并不是独立运行的,它需要运行在宿主环境中, 所以其实用户代理(user agent
)或者称为运行环境或者宿主环境,也就是浏览器。
每个用户代理必须至少有一个
browsing context event loop
(opens new window),但每个unit of related similar-origin browsing contexts
(opens new window) 最多只能有一个。
关于unit of related similar-origin browsing contexts
(opens new window),节选一部分规范的介绍:
Each unit of related browsing contexts (opens new window) is then further divided into the smallest number of groups such that every member of each group has an active document (opens new window) with an origin (opens new window) that, through appropriate manipulation of the document.domain attribute, could be made to be same origin-domain (opens new window) with other members of the group, but could not be made the same as members of any other group. Each such group is a unit of related similar-origin browsing contexts.
简而言之就是一个浏览器环境(unit of related similar-origin browsing contexts.
),只能有一个事件循环(event loop
)。
# event loop
做了什么?
每个
event loop
都有一个或多个 task queues. 一个 task queue (opens new window)是tasks
的有序的列表, 是用来响应如下工作的算法:
事件
在
EventTarget
触发的时候发布一个事件Event
对象,这通常由一个专属的 task 完成。注意:并不是所有的事件都从是
task queue
(opens new window)中发布,也有很多是来自其他的 tasks。解析
HTML
解析器 (opens new window)令牌化然后产生token
的过程,是一个典型的task
。回调函数
一般使用一个特定的
task
来调用一个回调函数。
使用资源(其实就是网络)
当算法 获取 (opens new window) 到了资源,如果获取资源的过程是非阻塞的,那么一旦获取了部分或者全部的内容将由
task
来执行这个过程。响应
DOM
的操作有一些元素会对
DOM
的操作产生task
,比如当元素被 插入到document
时 (opens new window)。
可以看到,一个页面只有一个event loop
,但是一个 event loop
可以有多个task queues
每个来自相同
task source
并由相同event loop
(比如,Document
的计时器产生的回调函数,Document
的鼠标移动产生的事件,Document
的解析器产生的tasks
) 管理的task
都必须加入到同一个task queue
中,可是来自不同task sources
(opens new window) 的tasks
可能会被排入到不同的task queues
中。
规范对task source
进行了分类:
如下
task sources
被大量应用于本规范或其他规范无关的特性中:
DOM
操作的task source
这种
task source
用来对DOM
的操作进行反应,比如像inserted into the document
的非阻塞的行为。用户操作的
task source
这种
task source
用来响应用户的反应,比如鼠标和键盘的事件。这些用来反应用户输入的事件必须由user interaction task source
(opens new window)来触发并排入tasks queued
。网络
task source
这种
task source
用来反应网络活动的响应。时间旅行
task source
这种
task source
用来将history.back()
等API
排入task queue
。
规范中明确表示了是有多个task queues
,并举例说明了这样设计的意义:
举例来说,一个用户代理可以有一个处理键盘鼠标事件的
task queue
(来自user interaction task source
),还有一个task queue
来处理所有其他的。用户代理可以以75%
的几率先处理鼠标和键盘的事件,这样既不会彻底不执行其他task queues
的前提下保证用户界面的响应, 而且不会让来自同一个task source
的事件顺序错乱。
接着:
当用户代理将要排入任务时,必须将任务排入相关的
event loop
的task queues
。
这句话很关键,是用户代理(宿主环境/运行环境/浏览器)来控制任务的调度,这里就引出了下面的Web APIs
。
接下来我么来看看event loop
是如何执行task
的。
# 处理模型
event loop
会在整个页面存在时不停的将task queues
中的函数拿出来执行,具体的规则如下:
一个event loop
在它存在的必须不断的重复一下的步骤:
- 从
task queues
中取出event loop
的最先添加的task
,如果没有可以选择的task
,那么跳到第Microtasks
步。 - 设定
event loop
当前执行的task
为上一步中选择的task
。 执行:执行选中的task
。 - 执行:执行选中的
task
。 - 将
event loop
的当前执行task
设为null
。 - 从
task queue
中将刚刚执行的task
移除。 Microtasks
: 执行microtask
检查点的任务 (opens new window)。- 更新渲染,如果是浏览器环境中的
event loop
(相对来说就是Worker
中的event loop
)那么执行以下步骤: - 如果是
Worker
环境中的event loop
(例如,在WorkerGlobalScope
(opens new window)中运行),可是在event loop
的task queues
中没有tasks
并且WorkerGlobalScope
(opens new window)对象为关闭的标志,那么销毁event loop
,终止这些步骤的执行,恢复到run a worker
(opens new window)的步骤。 - 回到第
1
步。
# microtask
规范引出了microtask
,
每个
event loop
都有一个microtask queue
。microtask
是一种要排入microtask queue
的而不是task queue
的任务。有两种microtasks
(opens new window):solitary callback microtasks
和compound microtasks
。
规范只介绍了solitary callback microtasks
(opens new window),compound microtasks
可以先忽略掉。
当一个
microtask
要被排入的时候,它必须被排入相关event loop
的microtask queue,microtask
的task source
是microtask task source
.
# microtasks
检查点
当用户代理执行到了microtasks
检查点的时候,如果performing a microtask checkpoint flag
(opens new window)为 false
,则用户代理必须运行下面的步骤:
- 将
performing a microtask checkpoint flag
置为true
。 - 处理
microtask queue
:如果event loop
的microtask queue
是空的,直接跳到Done
步。 - 选择
event loop
的microtask queue
中最老的microtask
。 - 设定
event loop
当前执行的task
为上一步中选择的task
。 - 执行:执行选中的
task
。
注意:这有可能涉及相关的脚本回调函数,这些回调函数最后都会执行
clean up after running script
(opens new window),这回导致再次执行microtask
检查点的任务 (opens new window),这就是我们要使用performing a microtask checkpoint flag
的原因。
- 将
event loop
的当前执行task
设为null
。 - 将上一步中执行的
microtask
从microtask queue
中移除,然后返回 处理microtask queue
步骤。 - 完成: 对每一个
responsible event loop
(opens new window)就是当前的event loop
的environment settings object
(opens new window),给environment settings object
(opens new window)发一个rejected promises
(opens new window)的通知。 - 清理
IndexedDB
的事务 (opens new window)。 - 将
performing a microtask checkpoint flag
设为false
。
为啥要用microtask
?根据HTML Standard
,在每个 task
运行完以后,UI
都会重渲染,那么在microtask
中就完成数据更新,当前task
结束就可以得到最新的UI
了。反之如果新建一个task
来做数据更新,那么渲染就会进行两次。
整个流程如下图:
# task & microTask
task
主要包含:
script
(整体代码)setTimeout
setInterval
setImmediate
I/O
UI rendering
microtask
主要包含:
process.nextTick
(Node.js
环境)Promises
(这里指浏览器实现的原生Promise
)Object.observe
(已被MutationObserver
替代)MutationObserver
postMessage
# Web APIs
在上面讲到了用户代理(宿主环境/运行环境/浏览器)来控制任务的调度,task queues
只是一个队列,它并不知道什么时候有新的任务推入,也不知道什么时候任务出队。event loop
会根据规则不断将任务出队,那谁来将任务入队呢?答案是 Web APIs
。
我们都知道JavaScript
的执行是单线程的,但是浏览器并不是单线程的,Web APIs
就是一些额外的线程,它们通常由C++
来实现,用来处理非同步事件比如DOM
事件,http
请求,setTimeout
等。他们是浏览器实现并发的入口,对于Node.JavaScript
来说,就是一些C++
的APIs
。
WebAPIs
本身并不能直接将回调函数放在函数调用栈中来执行,否则它会随机在整个程序的运行过程中出现。每个WebAPIs
会在其执行完毕的时候将回调函数推入到对应的任务队列中,然后由event loop
按照规则在函数调用栈为空的时候将回调函数推入执行栈中执行。event loop
的基本作用就是检查函数调用栈和任务队列,并在函数调用栈为空时将任务队列中的的第一个任务推入执行栈中,每一个任务都在下一个任务执行前执行完毕。
WebAPIs
提供了多线程来执行异步函数,在回调发生的时候,它们会将回调函数和推入任务队列中并传递返回值。
# 参考文献
Tasks, microtasks, queues and schedules (opens new window)