前言
说到浏览器事件循环,就要对浏览器的工作原理进行了解,浏览器有几个进程、每个进程又有多少线程,从网页输入地址到渲染页面的流程是什么,等等,本篇文章整理了并捋清楚这些内容
浏览器的进程模型
说到浏览器的事件循环,得首先了解下浏览器的工作原理,只有了解了其工作原理才能对事件循环进一步的解释
何为进程?
程序运行需要有自己专属的内存空间,可以把这块内存空间简单理解为进程
在计算机中,进程(process)是指正在运行的一个程序。进程是操作系统资源分配的基本单位,包括了程序全部需要的代码、数据和所需的系统资源
进程可以分为前台进程和后台进程,前台进程是用户正在交互使用的应用程序,这里浏览器就是前台进程;后台进程则是在后台运行的,通常是一些不需要用户交互的服务程序
每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意
何为线程?
有了进程后,就可以运行程序的代码了,运行代码的人称之为线程
一个进程至少有一个线程,所以在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程
如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程
目前主流的浏览器有哪些?
什么叫主流的浏览器,就是拥有独立自主开发的内核
- Navigator(Gecko):1994年网景公司开发的 Gecko为内核
- Opera(Preston/Blink):欧朋浏览器
- IE(Trident)->Edge(Chromium):微软公司开发的 Chromium为内核
- FireFox(Gecko):Mozilla(MDN)网景公司的一个组织
- Safari(Webkit):苹果公司开发的 Webkit内核
- Chrome(Webkit->Blink):也是苹果公司开发的,Blink内核是在Webkit基础上进化的
浏览器有哪些进程和线程?
浏览器是一个多进程和多线程的应用程序
可以在浏览器的任务管理器中查看当前的所有进程
其中,最主要的进程有:
-
浏览器进程
主要负责界面显示、用户交互、子进程管理等。浏览器内部会启动多个线程处理不同的任务
-
网络进程
负责加载网络资源,网络进程内部会启动多个线程来处理不同的网络任务
-
渲染进程
渲染进程启动后,会开启一个渲染主线程,主线程负责执行HTML、CSS、JS代码,默认情况下,浏览器会为每个标签页开启一个新的渲染进程,已保证不同的标签页之间不相互影响
渲染主线程是如何工作的
渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:
- 解析HTML
- 解析CSS
- 计算样式
- 布局
- 处理图层
- 每秒把页面画60次
- 执行JS全局代码
- 执行事件处理函数
- 执行计时器的回调函数
- ......
渲染主线程为了处理这么多的任务能够有效及时的响应,通过排队调度任务
- 最开始的时候,渲染主线程会进入一个无限循环
- 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态
- 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务
这样一来,就可以让每个任务有条不紊的、持续的进行下去了
整个过程,被称之为事件循环(消息循环)
何为异步?
代码在执行过程中,会遇到一些无法立即处理的任务,比如:
- 计时完成后需要执行的任务 -- setTimeout、setInterval
- 网络通信完成后需要执行的任务 -- XHR、Fetch
- 用户操作后需要执行的任务 -- addEventListener
如果让渲染主线程等待这些任务的时机到达,就会导致主线程长时间处于阻塞的状态,从而导致浏览器卡死
渲染主线程承担着极其重要的工作,无论如何都不能阻塞!
因此,浏览器选择异步来解决这个问题
使用异步的方式,渲染主线程永不阻塞
任务有优先级吗?
任务是没有优先级的,在消息队列中先进先出
但是消息队列是有优先级的
根据W3C的最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在同一个队列中,不同类型的任务可以分属于不同的队列
- 在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行
- 浏览器必须准备一个微队列,微队列中的任务优先所有其他任务执行
随着浏览器的复杂度急剧提升,W3C不再使用宏队列的说法
在目前chrome的实现中,至少包含了以下几个队列:
- 微队列:主要负责处理Promise回调函数、MutationObserver回调函数等,优先级最高
- 渲染队列:也称为重绘队列,用于存放页面的回流与重排任务,这些任务通常是由JavaScript修改DOM或样式等引起的,当渲染队列中存在任务时,浏览器会优先处理,会对任务进行排队和合并,以减少页面的回流与重绘次数,优先级高
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级中
- 延时队列:用于存放计时器到达后的回调函数,优先级低
单线程是异步产生的原因 事件循环是异步实现的方式
浏览器的渲染原理
渲染时间点
当网络线程接收到HTML文件时候,渲染主线程会把这个文件一步步解析、构成渲染dom树对象,再进行计算样式绘制节点等操作,最后由GPU渲染成页面,中间有很多复杂繁琐的细节
渲染流水线
HTML解析过程中遇到CSS代码怎么办?
为了提高解析效率,浏览器会启动一个预解析器率先下载和解析CSS
HTML解析过程中遇到JS代码怎么办?
渲染主线程遇到JS代码时必须暂停一切行为,等待下载执行完后才能继续,预解析线程可以分担一点下载JS的任务
当script脚本中有属性async和defer时候,都会推迟脚本执行,但浏览器会先下载。 其中async,浏览器会先下载,然后下载完后会立即执行,这样会暂停HTML解析,因为可能会JS改变DOM结构 而defer会浏览器先下载,当HTML解析完页面后再执行,不会阻塞HTML的解析。 两者都会在load事件之前执行,但可能会在DOMContentLoaded之前或之后
其实渲染主线程的工作就到绘制这一步,剩下的步骤都是其他线程完成的,如合成线程等
总结
浏览器是如何渲染页面的?
当浏览器的网络线程收到HTML文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列
在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程
整个渲染流程分为多个阶段,分别是:HTML解析、样式计算、布局、分层、绘制、分块、光栅化、画
每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入
这样,整个渲染流程就形成了一套严密组织的生产流水线
1、渲染的第一步是解析HTML-Parse HTML
解析过程中遇到CSS解析CSS,遇到JS执行JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载HTML中的外部CSS和JS文件
如果主线程解析到link位置,此时外部的CSS文件还没有下载解析好,主线程不会等待,继续解析后续的HTML。这是因为下载和解析CSS的工作是在预解析线程中进行的。这就是CSS不会阻塞HTML解析的根本原因
如果主线程解析到script位置,会停止解析HTML,转而等待JS文件下载好,并将全局代码解析执行完成后,才能继续解析HTML。这是因为JS代码的执行过程可能会修改当前的DOM树,所以DOM树的生成必须暂停。这就是JS会阻塞HTML的解析的根本原因
第一步完成后,会会得到DOM树和CSSOM树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在CSSDOM树中
2、渲染的下一步是样式计算-Recalculate Style
主线程会遍历得到的DOM树,依次为树中的每个节点计算出它最终的样式,称之为Computed Style
在这一过程中,很多预设值会变成绝对值,比如red
会变成rgb(255,0,0)
;相对单位会变成绝对单位,比如em
会变成px
,这一步完成后,会得到一颗带有样式的DOM树
3、接下来是布局-Layout
布局完成后会得到布局树,布局阶段会依次遍历DOM树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置
大部分时候,DOM树和布局树并非一一对应
比如display:none
的节点没有任何几何信息,因此不会生成到布局树中;又比如使用了伪元素选择器,虽然DOM树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中
4、下一步是分层-Layer
主线程会使用一套复杂的策略对整个布局树进行分层
分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提高效率
滚动条、堆叠上下文、transform、opacity等样式都会或多或少的影响分层结果
5、再下一步是绘制-Paint
主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成
6、再接下来是分块-Tiling
合成线程首先会对每个图层进行分块,将其划分为更多的小区域,它会从线程池中拿取多个线程来完成分块工作
7、下一步是光栅化-Raster
分块完成后,进入光栅化阶段
合成线程会将块信息交给GPU进程,以极高的速度完成光栅化,GPU进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块,光栅化的结果就是一块一块的位图
8、最后一步是画-Draw
合成线程拿到每个层,每个块的位图后,生成一个个指引信息,指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形,变形发生在合成线程,与渲染主线程无关,这就是transform效率高的本质原因
合成线程会把指引提交给GPU进程,由GPU进程产生系统调用,提交给GPU硬件,完成最终的屏幕成像
什么是reflow-回流(重排)?
reflow
的本质是重新计算layout树,当进行了会影响布局树的操作后,需要重新计算布局树,会引发layout
为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当JS代码全部完成后再进行统一计算。所以,改动属性造成的reflow是异步完成的
也同样因为如此,当JS获取布局属性时,就可能造成无法获取到最新的布局信息,浏览器在反复权衡下,最终决定获取属性立即reflow
什么是repaint-重绘?
repaint的本质就是重新根据分层信息计算了绘制指令,当改动了可见样式后,就需要重新计算,会引发repaint
由于元素的布局信息也属于可见样式,所以reflow一定会引起repaint
为什么transform效率高?
因为transform既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个draw阶段
由于draw阶段在合成线程中,所以transform的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响transform的变化