简洁地说一下 Event-Loop
Event-Loop 之前也学习过,先说一个整体的简洁的关于 Event-Loop 的普遍认识:
宏任务 -> 微任务 -> (微任务产生的微任务) -> 下一个宏任务
看起来就这些内容,但往细节里说,这里面还有很多知识点可以了解的,如 Event-Loop 跟浏览器的关系,rAF,rIC,一帧的时间里发生了什么事情等。下面就对 Event-Loop 进行深入一点的了解。
Event-Loop 跟浏览器的关系
在进入正题前,我们先了解一下浏览器是怎么渲染一个网页的:
访问链接,下载对应的 Html 文件,Css 文件,Js 文件,然后将 HTML 解析构建 DOM 树,将 CSS 解析为 CSSOM 树,然后将它们关联起来(Attachment过程),构建 Render 树,然后计算节点的位置(Layout),然后就将节点绘制出来(painting)。
这里面,浏览器做了哪些事情呢?
这得先了解一下浏览器中的一些进程信息:
浏览器进程:
浏览器最核心的进程,负责管理各个标签页的创建和销毁、页面显示和功能(前进,后退,收藏等)、网络资源的管理,下载等。
渲染进程:
浏览器会为每个窗口分配一个渲染进程、也就是我们常说的浏览器内核,这可以避免单个 page crash 影响整个浏览器。
从上面进程的介绍可以知道,浏览器进程的工作是:
访问链接后,浏览器进程将会将这个网站对应的资源下载下来,然后就到渲染进程来接手下面的工作了。
那么渲染进程,它要做什么工作呢?
从名称上就知道,这个进程,只要的工作,就是将内容渲染到屏幕上。那么它肯定会解析 HTML,也会执行 JavaScript ,当执行 JavaScript 时,有可能触发定时器,有可能触发事件,有可能会发异步 Http 请求等。
渲染进程会有一些线程来辅助完成以上的工作,大概有以下线程:
GUI渲染线程,定时器触发线程,事件触发线程,异步Http请求线程,JavaScript 引擎线程。
GUI渲染线程
GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。定时器触发线程
浏览器定时计数器并不是由
JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。事件触发线程
当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
异步Http请求线程
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理。
JavaScript 引擎线程
Javascript引擎,也可以称为JS内核,主要负责处理Javascript脚本程序,例如V8引擎。Javascript引擎线程理所当然是负责解析Javascript脚本,运行代码。
其中,由于 JavaScript 有可能会操作 DOM,所以,GUI渲染线程 跟 JavaScript引擎线程 是互斥的。
这些线程的交互,可以区分为任务的发起方跟任务接受方来看:
| 任务发起方 | 任务 | 任务接受方 |
|---|---|---|
| JavaScript引擎线程 | 代码 | JavaScript引擎线程 |
| JavaScript引擎线程 | 定时器 | 定时器触发线程 |
| JavaScript引擎线程 | Http请求 | 异步Http请求线程 |
| JavaScript引擎线程 | 触发事件 | 事件触发线程 |
| GUI渲染线程 | 渲染 | GUI渲染线程 |
以上都是浏览器的介绍,那么跟 Event-Loop 的 关系是啥呢?
我对 Event-Loop 的理解,更倾向是 JavaScript 与浏览器的交互和浏览器的调度行为。
比如由 JavaScript 触发定时器事件,浏览器将这个事件分配给定时器触发线程来进行计时,这样就不会阻塞代码的往下执行。
又如,JavaScript 发起异步 Http 请求,浏览器会将它分配给 异步 Http 请求线程,等待状态更改后,通知 JavaScript 来处理它的回调事件。
至于事件触发,就更体现 JavaScript 跟浏览器的交互了,用户点击页面上的一个元素,触发了一个事件,这个事件是由浏览器告诉给 JavaScript 的,然后 JavaScript 执行这个事件的对应的方法,有可能会触发定时器,或发异步请求,或对 DOM 进行操作,这就会牵涉到与其他线程的配合完成了。
这些过程,就是 宏任务 -> 微任务 -> (微任务) -> 下一个宏任务 的宏任务的具体表现了。
一帧时间里发生的故事
这个故事,得先介绍四位主要角色 rAF(requestAnimationFrame),rIC(requestIdleCallback),task(宏任务),render(绘制页面) 给大家认识。
一帧时间,有多长呢?我们屏幕刷新频率是 60次每秒,人眼看过去会觉得画面效果很流畅。所以一帧时间,就是 1000ms / 60 = 16.6ms 。
那么在这么短的时间内,到底发生了什么故事呢?
我们知道,假如没什么意外的话,render 肯定会在时间的最后,因为每一帧画面都要它来完成绘制。
我们也知道,task 一般来说会占据这一帧时间的绝大部分(每一个 task 完成后都会将当下的 microTask quene 执行完毕后再执行下一个 task ),因为 Event-Loop 在不停地执行。
那么,rAF 和 rIC 会在什么时刻出场呢?
因为 task 是绝对的主角,龙傲天的那种,render 是守门人,执行这一帧画面的绘制。所以当龙傲天完全退场了,守门人准备接过工作前,rAF 瞅准时机进场了,因为它的定义就是:
希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
当守门人完成它的工作后,发现,这短暂的一帧时间还有剩余的时候,佛系的 rIC 缓缓进场了,因为它的定义就是:
将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
为什么前文说到 task 是绝对的主角呢?
因为它非常容易搞事情,让这一帧的时间变得很有“意义”:
当 task 过多或执行时间过长,那么这一帧时间,就没 render,rAF,rIC 什么事情了。
从这里也知道,rAF,rIC 的调用时机是不准的。
总结
- 对
Event-Loop的理解,倾向于是JavaScript与浏览器的交互和浏览器的调度行为 - 一帧时间内,会清空当前的
task和microTask,如果还有时间剩余,将会执行rAF(假如有),render,rIC(假如有剩余时间和有调用rIC)
思考
其实还是有一些疑问在脑海中盘旋,render 真的是一帧时间里的守门人吗?render 后假如有 task 会不会继续执行 task 呢?还是等到下一帧呢?