前言
在讲述事件循环和消息队列之前,需要了解 JS 的单线程执行机制,JS 的执行是从上到下依次执行的,这些便是同步任务,而异步操作类似于系统中断,即当前进程外部的实体(主线程之外的、宿主环境提供的、特殊的线程,如IO线程(HTTP请求)和定时器线程等)可以触发代码执行,然后在异步任务完毕后,执行回调。
异步任务不同于顺序执行的同步任务,异步任务的回调函数对于 JS 运行时来说是一个黑盒,无法预知他究竟什么时候会被执行,因为这取决于异步回调何时从消息队列中出队执行,而消息队列中的异步回调是否出队,则与事件循环机制直接相关。
从多进程(process)和单线程(thread)谈起
人们使用的现代浏览器都是多进程的应用程序,而运行在浏览器上的 JS 代码是单线程的。
浅谈Chrome浏览器架构
如果自己设计一个浏览器,浏览器可以是哪种架构呢?
- 单进程架构(线程间进行通信)
- 多进程架构(进程间 IPC (Inter-Process Communication)通信)
如果你的浏览器要以单进程架构进行设计,需要在一个进程内实现网络、调度、存储、IO设备、渲染、插件等任务,当然你可以把这些任务分为若干个线程去执行,形成单进程多线程的浏览器架构。
但是由于这些任务在现在操作系统中越来越复杂,例如把网络、存储、渲染这些任务放在一个线程中,执行效率和性能越来越低下,且无法再向下拆分出类似线程的子空间,因为线程已经是最小的执行单位。
因此,为了强化浏览器的各个复杂功能,出现了多进程架构的浏览器,可以将网络、存储、渲染、IO、插件这些复杂任务分配给一个个单独的进程,这样每个进程又能向下拆分出多个线程,极大程度上强化了浏览器。
理解Chrome的多进程架构
Chrome也是基于多进程架构的现代浏览器,Chrome的主要进程组成如下:
-
Browser 进程:Tab之外的一切都有该进程处理。负责地址栏、书签栏、前进后退、网络请求、文件访问等;
-
Renderer 进程:负责一个 Tab 内所有和网页渲染有关的事情,是最核心的进程;
-
Plugin 进程:负责 Chrome 插件相关的任务;
-
GPU 进程:GPU进程与其他浏览器进程相隔离处理GPU任务,比如把浏览器绘制到屏幕上;
所有应用程序都要在OS的调度下基于CPU和GPU的计算才能运行。因为GPU要处理多个应用程序的的请求,浏览器的的GPU进程只是一个分量。GPU擅长处理图形,因此提供GPU计算的应用程序可以实现快速渲染和平滑交互。
Chrome 的每一个Tab 选项卡都拥有自己的 Renderer 进程,有三个 Tab 就意味着有三个不同的 Renderer 进程这样可以保证多个 Tab 之间互不影响,即使其中一个 Tab 没有响应,也不影响其他 Tab 的正常执行。然而,由于进程是 OS 中拥有资源的独立单位,多个 Tab 之间的数据是非共享的,这也意味着多个 Tab 都会有相同的 V8引擎初始化数据,这意味着更多的内存使用。
了解 Browser 浏览器进程
简单来说,在浏览器中,Tab之外的一切都归浏览器进程所接管,它包含3个主要的线程:
- UI thread UI线程:负责绘制和管理浏览器的按钮和输入框区域。
- Network thread 网络线程:负责处理网络堆栈以从互联网接收数据
- Storage thread 存储线程:负责控制文件访问
现在让我们来模拟一个在地址栏输入网址,并将网页呈现在浏览器上的过程
-
用户在地址栏中键入字符串,UI 线程会识别该字符串是 URL 还是搜索关键词。
Chrome中的地址既可以访问网页,同时又是个搜索框,这里假设我们输入的是 URL。
-
UI 线程通知网络线程开始进行导航,发起网络请求
-
读取响应数据,如果响应的是 HTML 文件,那么下一步会将该数据传递给渲染进程;但如果响应数据是一个压缩包或其它类型的文件,那么就意味着我们发送的是下载请求,所以需要把数据传递给下载管理器
-
UI线程负责找到渲染进程,通知它要进行网页渲染
-
此时数据和渲染进程都已经准备好,浏览器进程和渲染进程开启 IPC 传递数据,导航部分完成,开始进行网页渲染。
了解最为重要的 Renderer 渲染进程
渲染进程主要包括4个线程:
- Main thread 主线程:执行js、下载资源、计算样式、进行布局、绘制合成
- Raster thread 光栅线程
- Compositor thread 合成线程
- Worker thread 工作者线程
主线程的功能
-
下载资源:主线程可以通过 Browser 线程的 Network 线程下载图片、css、js等渲染DOM需要的资源文件
-
执行 JS:主线程在遇到
<script>
标签时会阻塞HTML文档的解析,并必须先下载、解析和执行js代码,why?因为 js 可以用document.write()
之类的东西改变 DOM 结构。这就是为什么会暂停HTML的解析,并等待js代码执行完毕后才能恢复。 -
计算样式:主线程会基于CSS选择器或者浏览器默认样式去进行样式计算,最终生成 computed style
-
进行渲染:主线程根据先后顺序以及层级关系对 DOM 元素进行渲染,通常会生成多个图层
-
最终合成:主线程将渲染后的多个 frame 帧合成,类似flash的帧动画和PS的图层
关于合成线程
- 浏览器滚动时,合成线程会创建一个新的合成帧发送给 GPU
- 合成线程工作与主线程无关,不同等待样式计算和 js 的执行,因此合成线程相关的动画比涉及到主线程重新计算样式和执行 js 的动画更加流畅
主线程、合成线程和光栅线程配合工作的过程
- 主线程主要遍历布局树,生成层树
- 栅格线程栅格化磁铁到 GPU
- 合成线程将此贴合成帧并通过 IPC 传递给 Browser进程,显示在屏幕上。
参考
Inside look at modern web browser (part 1)
浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务