Event-Loop 扩展

214 阅读7分钟

简洁地说一下 Event-Loop

Event-Loop 之前也学习过,先说一个整体的简洁的关于 Event-Loop 的普遍认识:

宏任务 -> 微任务 -> (微任务产生的微任务) -> 下一个宏任务

看起来就这些内容,但往细节里说,这里面还有很多知识点可以了解的,如 Event-Loop 跟浏览器的关系,rAFrIC,一帧的时间里发生了什么事情等。下面就对 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 在不停地执行。

那么,rAFrIC 会在什么时刻出场呢?

因为 task 是绝对的主角,龙傲天的那种,render 是守门人,执行这一帧画面的绘制。所以当龙傲天完全退场了,守门人准备接过工作前,rAF 瞅准时机进场了,因为它的定义就是:

希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

当守门人完成它的工作后,发现,这短暂的一帧时间还有剩余的时候,佛系的 rIC 缓缓进场了,因为它的定义就是:

将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应

为什么前文说到 task 是绝对的主角呢?

因为它非常容易搞事情,让这一帧的时间变得很有“意义”:

当 task 过多或执行时间过长,那么这一帧时间,就没 renderrAFrIC 什么事情了。

从这里也知道,rAFrIC 的调用时机是不准的。

总结

  1. Event-Loop 的理解,倾向于是 JavaScript 与浏览器的交互和浏览器的调度行为
  2. 一帧时间内,会清空当前的 taskmicroTask ,如果还有时间剩余,将会执行 rAF(假如有),renderrIC(假如有剩余时间和有调用 rIC

思考

其实还是有一些疑问在脑海中盘旋,render 真的是一帧时间里的守门人吗?render 后假如有 task 会不会继续执行 task 呢?还是等到下一帧呢?

参考

浏览器是如何调度进程和线程的?

一位摸金校尉决定转行前端

requestAnimationFrame

requestIdleCallback