Event loop
概念
事件循环是agent(浏览器、node)处理事件的一种模式, 这些事件包括: 用户交互、脚本、渲染、网络请求等.
专业名词
集合(set)
- 是计算机中的一种数据结构, 表示一个一个具有唯一值的集合, 没有特别的循序
宏任务(macrotask/tasks)
宏任务是表述方式, 指的是通过宏任务算法创建的任务.
微任务(microtask/jobs)
微任务是表述方式,指的是通过微任务算法创建的任务.
任务队列/宏任务队列(task queue / macrotask queue)
一系列(异步)task的集合
微任务队列 (microtask queue)
一系列微任务的队列, 这是真的先进先出(FIFO)的队列
赫兹
屏幕刷新频率
屏幕上的图形图像是由一个个因电子束击打而发光的荧光点组成,由于显像管内荧光粉受到电子束击打后发光的时间很短,所以电子束必须不断击打荧光粉使其持续发光。
电子枪从屏幕的左上角的第一行开始,从左至右逐行扫描,第一行扫描完后再从第二行的最左端开始至第二行的最右端,一直到扫描完整个屏幕后再从屏幕的左上角开始,这时就完成了一次对屏幕的刷新
单位是Hz(赫兹)
当显示器处于60Hz的刷新频率时会产生令人难受的频闪效应, 刷新频率要达到75HZ以上,人眼才不易感觉出屏幕的闪烁.
渲染时机(rendering opportunity)
根据硬件约束(如屏幕刷新频率)和其他因素(如页面性能)或文档的可见性状态是否为“可见”来确定, 是否有渲染数据, 渲染时机通常定期出现。(即每次渲染时间间隔应该大体相同,比如屏幕刷新频率75Hz(13.33ms)或屏幕刷新频率60Hz(16.66ms))
requestIdleCallback
每一次时间循环中, 满足相关条件就会执行后台任务的协同调度,的一个方法
let handle = window.requestIdleCallback(callback[, options])
callback
一个在事件循环空闲时即将被调用的函数的引用。函数会接收到一个名为 IdleDeadline 的参数,这个参数可以获取当前空闲时间以及回调是否在超时时间前已经执行的状态。
options 可选
包括可选的配置参数。具有如下属性:
timeout: 如果指定了timeout,并且有一个正值,而回调在timeout毫秒过后还没有被调用,那么回调任务将放入事件循环中排队(宏任务),即使这样做有可能对性能产生负面影响。
DOMHighResTimeStamp
double类型的时间计量类,精确至【5微妙】, 用于存储以毫秒为单位的时间值。
高分辨率时间(high resolution time/HRT)
不受系统时钟偏差影响的时间
任务队列注意点
- 一个事件循环中有一个或多个任务队列(task queues)
- 一个任务队列(a task queue)是多个任务的集合(set)
任务队列是一个集合, 不是队列, 因为事件循环处理模式的第一步是在所选任务队列中获取第一个可运行的任务, 而不是获取排在最先的那个任务
微任务队列注意点
- 微任务队列是一个队列结构
- 每个事件循环中都有一个微任务队列, 最开始是空的.
- 每个事件循环都有一个执行微任务检查点的布尔值, 并且初始值是false, 它用于防止微任务检查点的重复调用.
其他注意点
- 每一个浏览器的事件循环有一个 上次渲染时机 (last render opportunity time)的时间值, 初始值是0
- 每一个浏览器的事件循环有一个 最后空闲期的开始时间(last idle period start time) 的时间值, 初始值是0
常见的微任务
- Promise.then(catch\finally)
- Object.observe
- MutationObserver
- Process.nextTick (node独有)
- async await (Promise语法糖)
常见的宏任务
- 定时器 (setTimeout\setInterval)
- requestAnimationFrame (请求浏览器在下次重新绘制之前调用指定函数来更新动画, 建议根据浏览器的屏幕刷新频率来设置回调间隔)
- I/O (用户点击事件)
- 脚本 (script)
- UI渲染 (UI rendering)
- setImmediate (node独有)
事件循环执行步骤
事件循环只要存在,就必须持续运行以下步骤(浏览器事件循环):
-
在多个任务队列中选择一个至少包含一个可运行任务的任务队列, 如果没有这样的任务队列, 就跳到第7步, 执行微任务检查点
-
找到这个任务队列中最先的可运行任务,并将其从任务队列中删除。
-
将这个任务设置为事件循环当前正在运行的任务。
-
设置一个任务开始时间的变量, 值为当前的高分辨率时间。
-
执行这个任务。
-
将事件循环中正在运行的任务设置为null。
-
执行微任务检查点
-
设置一个是否有渲染时机的变量(hasARenderingOpportunity), 值为false。
-
把现在时间设置为当前的高分辨率时间。
-
通过任务开始时间, 现在时间报告任务的持续时间, 当然还做了一些其他操作.
-
更新渲染
-
如果当前是窗口时间循环, 且所有的任务队列里没有任务, 且微任务队列为空, 同时渲染时机变量(hasARenderingOpportunity)为false, 这时会执行:
-
根据一系列算法, 计算一个最后空闲时间,(computeDeadline ===
IdleDeadline.timeRemaining())假设现在显示屏的刷新频率是60Hz, 一次刷新就是16.66ms, 事件循环中的任务都做完了, 这个16.66ms里剩下没做的时间就是这个
-
把最后空闲时间(computeDeadline)给到一个空闲时间算法, 去完成后台任务的协同调度, 也就是requestIdleCallback
-
执行微任务检查点步骤
-
如果事件循环执行的微任务检查点为true,则返回
-
将执行微任务检查点的事件循环设置为true
-
当事件循环的微任务队列不为空时:
- 把最先的微任务从队列拿出来。
- 将事件循环当前正在运行的任务设置为最先的微任务。
- 运行这个微任务。
- 将事件循环当前正在运行的任务设置回null。
-
在事件循环相应的执行环境中, 保存一些变量.
-
清理与当前事件循环匹配的一些事务.
-
执行ClearKepObject(), 存储一些记录.
参考文献: