1.浏览器(不定期更新...)

88 阅读14分钟

1 事件循环(Event loop) -- 浏览器的进程模型

1.1 进程

进程是在系统中能够独立运行并作为资源分配的基本单位,简单来说就是专属于自己的空间,供自己调配,每个应用至少有一个进程,进程之间相互独立,即使通信,也要对方同意才行

1.2 线程

有了进程后,就可以运行程序的代码了。 运行代码的「人」称之为「线程」

一个进程可以包含多个线程,但至少有一个线程。所以进程在开启前会自动创建一个线程运行代码,该线程称为主线程,如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码。

image.png

1.3 浏览器的进程和线程

浏览器是一个多进程多线程的应用程序,为了避免相互影响,启动时会自动启动多个进程

其中最主要的进程是:

  1. 浏览器进程 主要负责界面显示,用户交互,子进程管理等,浏览器进程内部会启动多个线程处理不同的任务
  2. 网络进程 负责加载网络资源,网络进程内部会启动多个线程来处理不同的网络任务
  3. 渲染进程 渲染进程启动后,会开启以一个渲染主线程,负责执行HTML,CSS,JS代码,默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同标签页不相互影响

1.4 渲染主线程如何工作 -- 排队

渲染主线程是浏览器中最繁忙的线程,需要他处理的任务包括但是不限于:

  • 解析HTML

  • 解析CSS

  • 计算样式

  • 布局

  • 处理图层

  • 每秒把页面画60次

  • 执行全局JS代码

  • 执行事件处理函数

  • 执行计数器的回调函数

  • ...

要处理这么多任务,我们需要用排队的方式调度这些任务

  • 1.最开始的时候,渲染主线程会进入一个无限循环
  • 2.每一次循环会检查消息队列中是否有任务存在,如果有,就取出第一个任务,如果没有,就进入休眠状态
  • 3.其他线程(包括其他进程的线程)可以随时向消息队列中添加任务,添加到消息队列的末尾,如果主线程是休眠状态,则会唤醒继续循环拿取任务

这样一来就可以让每个任务有条不紊的,持续进行下去了 ,我们将其过程称之为事件循环(消息循环)

1.5 何为异步

代码在执行过程中会遇到无法立即处理的任务:

  • 计时完成后需要执行的任务--setTimeOut setInterval
  • 网络通信完成后需要执行的任务--XHR,fetch
  • 用户操作后需要执行的任务 -- addEventListener 如果让渲染主线程等待这些任务的时机未达到,就会导致渲染主线程长期处于阻塞的状态,从而使浏览器卡死

image.png

渲染主线程承担着极其重要的工作,无论如何都不能阻塞,所以浏览器使用异步解决问题

image.png

使用异步的方式,渲染主线程永不阻塞

1.6 如何理解js的异步

js是一门单线程语言,因为它运行在浏览器的渲染主线程里面,而渲染主线程承担着诸多工作,渲染页面,执行js都在其中,如果使用同步的方式,就可能导致主线程阻塞,从而导致消息队列中很多任务无法执行这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。 在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。

所以浏览器采用异步的方式来避免,具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。 在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。

1.7 JS为什么会阻碍渲染

渲染被阻塞的原因很明显:因为Paint任务没有及时执行,即绘制列表没有及时提交给合成线程。

之所以没有及时执行,可能是因为JS执行时间过长,导致这一帧没有时间执行Paint

JS之所以阻塞渲染,是因为JS执行与渲染相关任务都在争夺主线程有限的资源。

JS执行时间过长,渲染相关任务就没时间执行了

1.8 任务中有优先级吗

任务中没有优先级,在消息队列中先进后出,但是消息队列是有优先级的

根据W3C的最新解释:

  • 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
  • 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行 html.spec.whatwg.org/multipage/w…

随着浏览器的复杂度急剧提升,W3C不再使用宏队列的说法

在目前chrome的实现中,至少包含了下面的队列:

  • 延时队列:用于存放计时器到达后的回调任务,优先级「中」
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
  • 微队列 microtask queue:用户存放需要最快执行的任务,优先级「最高」

添加任务到微队列的主要方式主要是使用Promise、MutationObserver 例如:

image.png 浏览器还有很多其他的队列,由于和我们开发关系不大,不作考虑

面试题:阐述一下JS的事件循环 事件循环 又叫消息循环,是浏览器渲染主线程的工作方式。在chrome的源码中,他会开启一个不会结束的for驯悍,每次循环都会从消息队列中去除第一个任务,其他线程只要在合适时机添加到消息队列末尾即可。

过去把消息队列简单拆分为宏队列和微队列,这种已无法满足现在需求,需要更加灵活的方式

根据W3C解释,每个任务有不同的类型,同类型的任务必须在同一个队列中,不同队列有优先级,在一次事件循环中,由浏览器决定取哪一个队列的任务,但浏览器还要有一个微队列,微队列的任务一定具有最高的优先级,必须优先带调度执行

js中的计时器能做到精确计时吗 不行,因为: 1.计算机硬件没有原子钟,无法做到精确计时

2.操作系统的计时函数本身就有少量偏差,由于JS的计时器最终调用的是操作系统的函数,也就携带了这些偏差

3.按照W3C的标准,浏览器实现计时器时,如果嵌套层级超过5层,则会带有4毫秒的最少时间,这样在计时上时间少于4毫秒时又带来了偏差

4.受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差

1.9总结

单线程是异步产生的原因

事件循环是异步的实现方式

2 渲染原理

2.1 浏览器如何渲染(render)页面的

image.png

当浏览器的网络线程收到HTML文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列,在时间循环的机制作用下,渲染主线程取出消息队列中渲染任务,开启渲染流程

  • 整个渲染流程分为HTML解析、样式计算、布局、分层、绘制、分块、光栅化、画
  • 每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。这样,整个渲染流程就形成了一套组织严密的生产流水线

2.1.1 parse HTML -- 解析HTML

解析过程中遇到CSS解析CSS,遇到JS解析JS,为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载HTML中的 外部CSS文件 和 外部的JS文件

如果主线程解析到link位置,此时外部的CSS文件还没有下载解析好,主线程不会等待,继续解析后续的HTML。这是因为下载和解析CSS的工作是在预解析线程中进行的。这就是CSS不会阻塞HTML解析的根本原因

image.png

如果主线程解析到script位置,会停止解析HTML,转而等待JS文件下载好,并将全局代码解析执行完成后,才能继续解析HTML。这是因为JS代码的执行过程可能会修改当前的DOM树,所以DOM树的生成必须暂停。这就是JS会阻塞HTML解析的根本原因

image.png

第一步完成后,会得到DOM树和CSSOM树,浏览器的默认样式、内部样式、外部样式、行内样式均会包含在CSSOM树中

2.1.2 Recalculate Style 样式计算

主线程会遍历得到的DOM树,依次为树中的每个节点计算出它最终的样式,称之为Computed Style。 在这一过程中,很多预设值会变成绝对值,比如red会变成rgb(255,0,0);相对单位会变成绝对单位,比如em会变成px

计算每个DOM节点具体样式方法

主要分为两点

  • 继承规则:每个子节点会默认去继承父节点的样式,如果父节点中找不到,就会采用浏览器默认的样式,也叫UserAgent样式
  • 层叠规则:层叠是 CSS 的一个基本特征,比如:.box p {}

在计算完样式之后,所有的样式值会被挂在到window.getComputedStyle当中,也就是可以 通过JS来获取计算后的样式 样式计算的整个过程就是完成了DOM节点中每个元素的具体样式,计算过程中要遵循CSS的继承和层叠两条规则,最终输出的内容是每个节点DOM的样式,被保存在ComputedStyle

这一步完成后,会得到一棵带有样式的DOM树

2.1.3 Layout 布局 布局完成后会得到布局树

布局阶段会依次遍历DOM树的每一个结点,计算每个结点的集合信息(计算出每个节点在屏幕中的位置),例如结点的宽高,大部分时候,DOM树和布局树并非一一对应,比如display:none的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然DOM树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致DOM树和布局树无法一一对应

<div>
    Some text
    <p>More text</p>
</div>

匿名块盒: 如果一个块盒(如上面的div),包含一个另一个块盒(如上面的p), 那么css强制这个块盒只包含块盒,因此会为Some text生成一个匿名块盒 匿名行盒:任何直接存在于一个块盒里面的文本(即文本没有被行盒元素包裹),都会被视为行盒,会生成一个匿名行盒包围它们

Layout Tree概念

DOM 树还含有很多不可见的元素,比如 head 标签,还有使用了 display:none 属性的元素。所以在显示之前,我们还要 额外地构建一棵只包含可见元素的布局树Layout Tree

结合下图来看看Layout Tree的构造过程:

image.png 从上图可以看出,DOM 树中所有不可见的节点都没有包含到布局树中

为了构建布局树,浏览器大体上完成了下面这些工作:

  • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中
  • 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树

以下都是布局树里面的信息

image.png image.png

2.1.4 Layer 分层

主线程会使用一套复杂的策略对整个布局树中进行分层。 分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。 滚动条、堆叠上下文、transform、opacity等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果。

单独分层
<style>
div {
   will-change: transform;
    //告诉浏览器这个元素那个属性可能要分层
}
</style>

2.1.5 Paint 绘制

主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。 完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。 合成线程首先对每个图层进行分块,将其划分为更多的小区域。 它会从线程池中拿取多个线程来完成分块工作。

canvas 就是调用浏览器的绘制功能

2.1.6 Tilling 分块(合成线程)

完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。 合成线程首先对每个图层进行分块,将其划分为更多的小区域。 它会从线程池中拿取多个线程来完成分块工作

2.1.7 Raster 光栅化

合成线程会将块信息交给GPU进程,以极高的速度(GPU加速)完成光栅化: GPU进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块。

光栅化的结果,就是一块一块的位图

2.1.8 Draw 画

合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。 指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。 变形发生在合成线程,与渲染主线程无关,这就是transform效率高的本质原因。 合成线程会把quad提交给GPU进程,由GPU进程产生系统调用,提交给GPU硬件,完成最终的屏幕成像。

image.png

2.2 reflow

reflow的本质就是重新计算layout树

当进行了会影响布局树的操作后,需要重新计算布局树,会引发layout。 为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当JS代码全部完成后再进行统一计算。所以,改动属性造成的reflow 是 异步完成的。

也同样因为如此,当JS获取布局属性时,就可能造成无法获取到最新的布局信息。 浏览器在反复权衡下,最终决定获取属性立即reflow。

2.3 repaint

repaint的本质就是重新根据分层信息计算了绘制指令。 当改动了可见样式后,就需要重新计算,会引发repaint。

由于元素的布局信息也属于可见样式,所以reflow一定会引起repaint。

2.4 为什么transform效率高

因为transform既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个「draw」阶段 由于draw阶段在合成线程中,所以transform的变化几乎不会影响渲染主线程。反之,渲染主线程无论如何忙碌,也不会影响transform的变化。