事件循环和消息队列(一)

前言

在讲述事件循环和消息队列之前,需要了解 JS 的单线程执行机制,JS 的执行是从上到下依次执行的,这些便是同步任务,而 ES6 引入了 Promise 对象,使得异步任务开始频频出现在 JS 的代码中。

异步任务不同于顺序执行的同步任务,他对于 JS 运行时来说是一个黑盒,无法预知他究竟什么时候会被执行,因为这取决于异步任务何时从消息队列中出队执行,而消息队列中的异步任务是否出队,则与事件循环机制直接相关。

从多进程(process)和单线程(thread)谈起

人们使用的现代浏览器都是多进程的应用程序,而运行在浏览器上的 JS 代码是单线程的。

浅谈Chrome浏览器架构

​ 如果自己设计一个浏览器,浏览器可以是哪种架构呢?

  • 单进程架构(线程间进行通信)
  • 多进程架构(进程间 IPC (Inter-Process Communication)通信)

如果你的浏览器要以单进程架构进行设计,需要在一个进程内实现网络、调度、存储、IO设备、渲染、插件等任务,当然你可以把这些任务分为若干个线程去执行,形成单进程多线程的浏览器架构。

但是由于这些任务在现在操作系统中越来越复杂,例如把网络、存储、渲染这些任务放在一个线程中,执行效率和性能越来越低下(比如有一些网页的代码存在内存泄露,即便关闭这些网页线程,进程中的这块内存也无法被回收,除非关闭浏览器,否则越用越卡),且无法再向下拆分出类似线程的子空间,因为线程已经是最小的执行单位。

因此,为了强化浏览器的各个复杂功能,出现了多进程架构的浏览器,可以将网络、存储、渲染、IO、插件这些复杂任务分配给一个个单独的进程,这样每个进程又能向下拆分出多个线程,极大程度上强化了浏览器。

理解Chrome的多进程架构

Chrome也是基于多进程架构的现代浏览器,Chrome的主要进程组成如下:

image-20230329211758826

  • Browser 进程:Tab之外的一切都有该进程处理。负责地址栏、书签栏、前进后退、网络请求、文件访问等;

  • Renderer 进程:负责一个 Tab 内所有和网页渲染有关的事情,是最核心的进程;

  • Plugin 进程:负责 Chrome 插件相关的任务;

  • GPU 进程:GPU进程与其他浏览器进程相隔离处理GPU任务,把浏览器的页面内容绘制到屏幕上;

    所有应用程序都要在OS的调度下基于CPU和GPU的计算才能运行。因为GPU要处理多个应用程序的的请求,浏览器的的GPU进程只是一个分量。GPU擅长处理图形,因此提供GPU计算的应用程序可以实现快速渲染和平滑交互。

image-20230329214757221

Chrome 的每一个Tab 选项卡都拥有自己的 Renderer 进程,有三个 Tab 就意味着有三个不同的 Renderer 进程这样可以保证多个 Tab 之间互不影响,即使其中一个 Tab 没有响应,也不影响其他 Tab 的正常执行。然而,由于进程是 OS 中拥有资源的独立单位,多个 Tab 之间的数据是非共享的,这也意味着多个 Tab 都会有相同的 V8引擎初始化数据,这意味着更多的内存使用。

image-20230329220338191

了解 Browser 浏览器进程

简单来说,在浏览器中,Tab之外的一切都归浏览器进程所接管,它包含3个主要的线程:

  • UI thread UI线程:负责绘制和管理浏览器的按钮和输入框区域。
  • Network thread 网络线程:负责处理网络堆栈以从互联网接收数据
  • Storage thread 存储线程:负责控制文件访问

而根据浏览器的优化策略,这三个线程往往会独立为三个进程。

现在让我们来模拟一个在地址栏输入网址,并将网页呈现在浏览器上的过程

  1. 用户在地址栏中键入字符串,UI 线程会识别该字符串是 URL 还是搜索关键词。

    Chrome中的地址既可以访问网页,同时又是个搜索框,这里假设我们输入的是 URL。

  2. UI 线程通知网络线程开始进行导航,发起网络请求

  3. 读取响应数据,如果响应的是 HTML 文件,那么下一步会将该数据传递给渲染进程;但如果响应数据是一个压缩包或其它类型的文件,那么就意味着我们发送的是下载请求,所以需要把数据传递给下载管理器

  4. UI线程负责找到渲染进程,通知它要进行网页渲染

  5. 此时数据和渲染进程都已经准备好,浏览器进程和渲染进程开启 IPC 传递数据,导航部分完成,你会发现tab由原网页台跳转到空白页面,然后开始边传输HTML 边进行网页渲染。

了解最为重要的 Renderer 渲染进程

渲染进程主要包括4个线程:

  • Main thread 主线程:执行JS、下载资源、计算样式、进行布局、绘制合成
  • Raster thread 光栅线程
  • Compositor thread 合成线程
  • Worker thread 工作者线程

主线程的功能

  • 执行 JS:主线程在遇到 <script> 标签时会阻塞HTML文档的解析,并必须先下载、解析和执行js代码,why?。因为浏览器需要一个稳定的 DOM 树结构,而 JavaScript 中的代码可能直接改变了 DOM 树的结构,甚至 直接使用 location.href 进行跳转,所以浏览器为了防止出现 JavaScript 改变 DOM 树的情况,会阻塞其他的下载和渲染。

  • 下载外部资源:如果HTML中由需要加载外部资源的标签,这在解析HTML构建DOM树之前会由预加载扫描线程检测到,并提前利用 Browser 线程的 Network 线程来下载<img/>、CSS和 JS的<link>等渲染DOM需要的外部资源文件,这减少了解析 HTML 的阻塞时间

  • 解析HTML:由 HTML解析器解析 HTML 内容,首先由分词器检测出各个标签名,我们称他们为token,然后利用token栈和括号匹配算法,构建出DOM树。同时会根据外部、内部和内联 CSS 样式计算得到 CSSOM 树。

    根据html文档生成DOM树

  • 计算CSS样式:主线程根据 CSSOM 树进行CSS属性值的计算,并将计算后的样式添加到DOM树的对应DOM节点上。

    :boat: 计算(最终)样式(computed style):是把继承、层叠关系理清,并且把所有CSS属性都赋值之后的CSS样式。

    :warning:HTML本质上只是提供了语义化的标签。为何div、p标签是块盒,而span标签却是行盒?根本原因是浏览器的源码中,设置了浏览器默认样式,而这些标签分别被设置为了display:block display:inline

  • 确定布局结构Layout:只有DOM节点和和它的样式可不够,还需要确定他们之间的布局关系,并构建与DOM树类似的布局树,比如在页面上的位置、盒子的尺寸大小的信息

    :label:布局树通常情况下与DOM树结构并不相同。由于布局树只考虑存在位置和尺寸这样的几何信息的DOM元素,所以display:none的DOM元素在构建布局树时是不被考虑的,类似的,还有一些伪元素,匿名行盒,匿名块盒…..

    :man_teacher: W3C规定:标签的文本必须被包含在行盒中;行盒和块盒不能相邻。因此用匿名行盒和匿名块盒来适应这个规定

    微信截图_20230411173336

  • 分层(Layer):主线程会使用一套复杂的策略对整个布局树中进行分层。分层的好处在于,将来某一个层发生改变后,仅会对该层进行后续处理,从而提升效率

    滚动条、层叠上下文(z-index)、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结构。

    分层不是越多越好,层数太多会导致占用大量的内存空间,因为浏览器会根据内存和效率权衡分层的数量。

    image-20230329230708934

  • 计算绘制指令集(paint):主线程会为每个层单独产生绘制指令集,用于描述这个层的内容该如何一步步地画出来。完成此步后,主线程将绘制指令集交付给合成线程

    微信截图_20230411174231

合成器线程

​ 一旦确定了绘制指令集,主线程就会将该信息提交给合成器线程。然后,合成线程将对每个层进行分块并光栅化。一个层可以比视口要大,所以合成器线程将它们划分为瓦片(图块),并将每个瓦片发送到GPU进程,完成光栅化,并且在这个过程中优先光栅化靠近视口的区域,紧接着再去光栅化页面的其他区域。(tiling和raster)

将这些信息转换为屏幕上的像素称为光栅化

​ 光栅化完成后,GPU进程将生成的位图交回给合成线程,合成线程收到每个层、每个块的位图之后,生成一个个的指引(quad)信息。指引信息会表示出每个位图应该滑到屏幕的那个位置,以及会考虑到旋转、缩放等变形。然后合成线程将 quad 提交给GPU进程,由GPU进程产生系统调用,提交给GPU硬件,完成最终的屏幕成像。(draw)

由于变形操作是在合成线程中执行的,与渲染主线程无关,这就是 transform 效率高的原因

  • 浏览器滚动时,合成线程会创建一个新的合成帧发送给 GPU,以显示到屏幕上,所以即便主线程卡死,也不影响页面滚动。
  • 合成线程工作与主线程无关,不用等待样式计算和 js 的执行,因此合成线程相关的动画比涉及到主线程重新计算样式和执行 js 的动画更加流畅

浏览器的渲染过程流程图

渲染流程图

参考

Inside look at modern web browser (part 1)

浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务

Inside look at modern web browser (part 2)

Inside look at modern web browser (part 3)


事件循环和消息队列(一)
http://example.com/2023/03/29/Broswer/
作者
Jabin
发布于
2023年3月29日
许可协议