图片
上文我们从偏JS调用机制的角度分析了,调用栈(Call Stack)/宏任务队列 (Task Queue)和微任务队列 (Microtask Queue)他们之间的关系和他们是如何协同合作的。并且,举了很多例子,用可视化的方式讲解它们如何工作的。
而今天,我们从浏览器内部的实现细节来谈谈EventLoop是如何从接受任务到渲染出对应页面的。
也就是下图中所涉及到的各个重要节点。在阅读完本文后,希望大家能对下面有一个清晰的认知。
图片
好了,天不早了,干点正事哇。
我们能所学到的知识点
- 前置知识点
- 事件循环(Event Loop)
- 任务队列/微任务队列/调用栈
- 在渲染队列中执行的是什么?
- EventLoop模型
1. 前置知识点
「前置知识点」,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。「如果大家对这些概念熟悉,可以直接忽略」同时,由于阅读我文章的群体有很多,所以有些知识点可能「我视之若珍宝,尔视只如草芥,弃之如敝履」。以下知识点,请「酌情使用」。
页面刷新术语
我们在页面是如何生成的(宏观角度)一文中提到过这些指标,这里就拿来主义了。
- 「屏幕刷新频率」
一秒内屏幕刷新的次数(一秒内显示了多少帧的图像),单位 Hz(赫兹),如常见的 60 Hz。「刷新频率取决于硬件的固定参数」(不会变的)。
- 「逐行扫描」
显示器并不是一次性将画面显示到屏幕上,而是「从左到右边,从上到下逐行扫描」,顺序显示整屏的一个个像素点,不过这一过程快到人眼无法察觉到变化。
以 60 Hz 刷新率的屏幕为例,这一过程即 1000 / 60 ≈ 16ms。
当扫描完一个屏幕后,设备需要「重新回到第一行」以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。
「帧率 (Frame Rate)」
表示 「GPU 在一秒内绘制操作的帧数」,如 60 fps,即每秒钟GPU最多绘制 60 帧画面。
帧率是「动态变化」的,例如当画面静止时,GPU 是没有绘制操作的,屏幕刷新的还是buffer中的数据,即GPU最后操作的帧数据。
「画面撕裂(tearing)」
一个屏幕内的数据来自2个不同的帧,画面会出现撕裂感。
测试帧率
我们可以借助requestAnimationFrame通过每个测量前后帧发生的时间间隔,来从侧面查看本地浏览器帧率。
const checkRequestAnimationDiff = () => {
let prev;
function call() {
requestAnimationFrame((timestamp) => {
if (prev) {
console.log(timestamp - prev);
// 应该大约是60FPS的16.6毫秒
}
prev = timestamp;
call();
});
}
call();
}
checkRequestAnimationDiff();
随意打开一个网站,并将上述代码贴到devtool-Console运行。
下面是,我们在React-官网[1]中实验的结果。
图片
从输出结果来看,虽然结果不是唯一,但是它们的值都稳定在16.67~16.68。和我们60fps是吻合的。
WebAPI
WebAPI工作的原理依赖于浏览器作为宿主环境来提供和执行这些API。在Web开发中,我们通常指的WebAPI是「浏览器内置的API」,它们允许开发者利用JavaScript与浏览器的功能进行交互。
APIs | 描述 |
网络请求 (Network Requests) | 使用 |
DOM操作 (DOM Manipulation) | 浏览器提供了一套DOM API,允许 |
事件处理 (Event Handling) | WebAPI允许注册事件处理程序来响应用户行为(如点击、滑动)或浏览器事件(如页面加载、窗口尺寸变化)。 |
存储机制 (Storage Mechanisms) | 浏览器提供了如 |
设备API (Device APIs) | 可以访问设备的硬件,如摄像头、麦克风、地理位置等,这通常通过 |
图形和动画 (Graphics & Animation) |
|
性能监控 (Performance Monitoring) |
|
其他API (Other APIs) | 还有诸如 |
WebAPI的工作流程
- 「调用API」:开发者在JavaScript代码中调用某个WebAPI。
- 「浏览器解释执行」: 浏览器解释JavaScript代码,并「执行相应的API调用」。
- 「API内部处理」:WebAPI内部可能会执行多种操作,如触发网络请求、访问数据库、启动硬件设备等。
- 「回调和事件循环」:对于异步操作,WebAPI通常会使用回调函数或Promise来处理操作完成后的结果。浏览器的事件循环机制确保了这些回调在适当的时候被调用。
- 「渲染和更新」:对于涉及视觉变化的API,如DOM操作或Canvas绘图,浏览器会更新页面内容,这通常发生在浏览器的下一个重绘周期。
在整个过程中,「浏览器的角色是中介」,它提供了执行API的环境和必要的安全措施。这些API让Web应用可以像本地应用一样丰富和强大,同时仍然运行在浏览器这个相对安全的沙箱环境中。
下面的图,展示了WebAPI的地位(中间部分)。
图片
GPU硬件加速
「GPU(Graphics Processing Unit)硬件加速」是一种利用GPU来执行图形和计算任务的技术。在Web开发中,GPU硬件加速可以通过利用用户计算机中的GPU资源来加速浏览器的渲染和绘制操作。这通常可以提高网页的性能和流畅度,尤其是对于需要大量图形操作的页面。
在Web开发中,一些CSS属性和操作可以触发GPU硬件加速,以便更有效地利用GPU资源。
- 3D 变换(transform)
使用transform属性进行3D变换,如translate3d、rotate3d、scale3d等,可以触发GPU硬件加速。例如:
.element {
transform: translate3d(0, 0, 0);
}
- CSS 动画(animation)和过渡(transition):
使用CSS动画和过渡属性,例如transform属性的过渡,可以触发GPU硬件加速。例如:
.element { transition: transform 0.3s ease-in-out; }
Canvas 绘图:
在
使用 will-change 属性:
will-change属性告诉浏览器某个属性将会被改变,从而可以提前进行优化。例如:
.element { will-change: transform; }
使用 image-rendering 属性:
image-rendering属性用于指定图像的渲染质量,而且在某些情况下也能触发GPU硬件加速。例如:
.element { image-rendering: pixelated; }
使用 backface-visibility 属性:
backface-visibility属性用于指定当元素不面向屏幕时是否可见。在某些情况下,该属性的使用可以触发GPU硬件加速。例如:
.element { backface-visibility: hidden; }
使用 filter 属性(某些情况下):
在某些情况下,使用filter属性(如模糊、对比度等)可能触发GPU硬件加速。
还记得我们在你会在浏览器中打断点吗?我会!中介绍过如何看chromium 在线仓库[2]
那我们就从源码的角度来看看,为什么上面的属性会走GPU硬件加速
或者我们可以看compositing_reason_finder.cc这个文件,它例举了很多枚举类型。
图片
2. 事件循环(Event Loop)
事件循环就是一个「死循环」,不死不休。
旧的操作系统不支持多线程,它们的事件循环可以被大致描述为一个简单的循环:
while (true) {
if (hasTasks()) {
executeTask();
}
}
现代操作系统的调度器(schedulers)非常复杂。它们有优先级设置、执行队列等许多其他技术。
这里做一个题外话,看到schedulers/优先级设置是不是想到React-Fiber架构了。其实,React在内部就是模仿操作系统,做了自己的实现逻辑。(这里就不展开说明了)
为了让事情简单化,我们可以将事件循环(Event Loop)描述为一个循环,该循环检查是否有任何待处理的任务:
图片
任务触发器
浏览器属于「事件驱动」的技术框架,如果想让Event Loop探查并执行对应的任务,首先要做的就是将某些任务进行触发。也就是唤起指定任务的触发器。
下面就是我们平时能够接触到的任务触发器
- 「