浏览器
浏览器由哪些进程和线程
浏览器是一个多进程,多线程的应用程序
浏览器内部工作极其复杂
为了避免相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程.
可以在浏览器的任务管理器中查看当前的所有进程
其中,最主要的进程有:
- 浏览器进程
主要负责界面显示,用户交互,子进程管理等.浏览器进程内部会启动多个线程处理不同的任务.
- 网络进程
负责加载网络资源.网络进程内部会启动多个线程来处理不同的网络任务.
- 渲染进程
渲染进程启动后,会开启一个渲染主线程,主线程负责执行HTML,CSS,JS代码.
默认情况下,浏览器会为每一个标签页开启一个新的渲染进程,以保证不同标签页之间互不影响.
将来该默认模式可能会有所改变,可以参见chrome官方说明文档
渲染主线程是如何工作的
渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于:
- 解析HTML
- 解析CSS
- 计算样式
- 布局
- 处理图层
- 每秒把页面画60次
- 执行全局JS代码
- 执行事件处理函数
- 执行计时器的回调函数
为什么渲染进程不适用用多个线程来处理这些事情?
要处理这么多的任务,主线程遇到了一个前所未有的难题:如何调度任务?
比如:
- 我正在执行一个JS函数,执行到一半的时候用户点击了按钮,我该立即去执行点击事件的处理函数吗?
- 我正在执行一个JS函数,执行到一半的时候,某个计时器达到了时间,我该立即去执行它的回调吗?
- 浏览器进程通知我用户点击了按钮,与此同时,某个计时器也到达了时间,我应该处理哪一个呢?
渲染主线程想出一个绝妙的办法来处理该问题:排队
- 在最开始的时候,渲染主线程会进入一个无限循环
- 每一次循环会检查消息队列中是否有任务存在.如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态
- 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务.新任务会加到消息队列的末尾.在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务.
这样一来,就可以让每个任务有条不紊,持续的进行下去了.
整个过程,称之为事件循环.
若干解释
什么是异步?
代码执行过程中,会遇到一些无法立即处理的任务,比如:
- 计时完成后需要执行的任务:
setTimeout
,setInterval
- 网络通信完成后需要执行的任务:
XHR
,Fetch
- 用户操作后需要执行的任务:
addEventListener
如果让渲染主线程等待这些任务的时机到达,就会导致主线程长期处于阻塞状态,从而导致浏览器卡死
渲染主线程承担着极其重要的工作,无论如何都不能阻塞!
因此,浏览器选择异步来解决这个问题.
使用异步的方式,渲染主线程永不阻塞
面试题: 如何理解JS的异步
JS是一门单线程语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个.
而渲染主线程承担着诸多工作,渲染页面,执行JS都在其中运行
如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法执行.
这样一来,一方面会导致繁忙的主线程白白地消耗时间,另一方面会导致页面无法及时更新,给用户造成卡死现象.
所以浏览器采用异步的方式来避免.具体做法是当某些任务发生时,比如计时器,网络,事件监听器,主线程将任务交给其他线程去处理,自身立即结束任务执行,转而执行后续代码.当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行
在这种异步模式下,浏览器永不阻塞,从而最大限度保证了单线程的流畅运行.
JS为何会阻碍渲染
先看代码
1 |
|
任务有优先级吗?
任务没有优先级,在消息队列中先进先出.
但是消息队列是有优先级的
根据W3C的最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在同一队列,不同类型的任务可以分属不同的队列.在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行.
- 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行
https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
随着浏览器的复杂度急剧提升,W3C不再使用宏队列的说法
在目前chrome的实现中,至少包含了下面的队列:
- 延时队列:用于存放计时器达到后的回调任务,优先级(中)
- 交互队列: 用于存放用户操作后残生的事件处理任务,优先级(高)
- 微队列: 用户存放需要最快执行的任务,优先级(最高)
面试题: 阐释一下JS的事件循环
事件循环,又叫做消息循环,是浏览器渲染主线程的工作方式.
在Chrome的源码中,它开启一个死循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要再合适的时候将任务放入到队列末尾即可.
过去把消息队列简单分为宏队列和微队列,这种说法目前已经无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式.
根据W3C的官方解释,每个任务有不同的类型,同类型任务必须在同一个队列,不同的任务可以属于不同的队列.不同任务队列有不同的优先级,在一次事件循环中,有浏览器自行决定取哪一个队列的任务.但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行.
面试题: JS中的计时器能够做到精确计时吗?为什么?
不行,因为:
- 计算机硬件没有原子钟,无法做到精确计时
- 操作系统的计时函数本身就有少量偏差,由于JS的计时器最终调用的是操作系统的函数,也就携带了这些偏差
- 根据W3C的标准,浏览器实现计时器时,如果嵌套层级超过5层,则会带有4ms的最少时间,这样也有误差
- 受事件循环影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差.
单线程是异步产生的原因
事件循环是异步的实现方式
浏览器渲染原理
渲染(render): html字符串 —> 像素信息
渲染时间点
渲染流水线
解析HTML-Parse HTML
样式计算 Recalculate Style
布局 Layout
内容必须在行盒中
行盒和块盒不能相邻
分层 Layer
绘制 Paint
这里的绘制,是为每一层生成绘制指令.
分块 Tiling
分块的工作是交给多个线程同时进行的.
光栅化 Raster
光栅化是将每个块变成位图
优先处理靠近视口的块.
此过程会用到GPU加速
画 Draw
合成线程计算出每个位图在屏幕上的位置,交给GPU进行最终呈现.
面试题
浏览器是如何渲染页面的?
当浏览器的网络线程收到HTML文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列.
在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程.
整个渲染流程分为多个阶段,分别是:HTML解析,样式计算,布局,分层,绘制,分块,光栅化,画.
每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入.
这样,整个渲染流程就形成了一套组织严密的生产流水线.
渲染的第一步是解析HTML
解析过程中遇到CSS解析CSS,遇到JS执行JS.为了提高解析效率,浏览器在开始解析前,会启动一个预解析线程,率先下载HTML中的外部CSS文件和外部JS文件.
如果主线程解析到link
位置,此时外部的CSS文件还没有下载解析好,主线程不会等待,继续解析后续的HTML.这是因为下载和解析CSS的工作是在预解析线程中进行的.这就是CSS不会阻塞HTML解析的根本原因.
如果主线程解析到script
位置(而且script
标签没有标记为async
或defer
),会停止解析HTML,转而等待JS文件下载好,并将全局代码解析执行完成后,才能继续解析HTML.这是因为JS代码执行过程可能会修改当前的DOM树,所以DOM树的生成必须暂停.这就是JS会阻塞HTML解析的根本原因.
第一步完成后,会得到DOM树和CSSOM树,浏览器的默认样式,内部样式,外部样式,行内样式均会包含在CSSOM树中.
第二步是样式计算
主线程会遍历得到的DOM树,依次为树中每个节点计算出它的最终样式,称之为Computed Style
在这一过程中,很多预设值会变成绝对值,比如red
会变成rgb(255,0,0)
;相对单位会变成绝对单位,比如em
会变成px
这一步完成后,会得到一棵带有样式的DMO树.
第三步是布局,布局完成后会得到布局树
布局阶段会依次遍历DOM树中的每一个节点,计算每个节点的几何信息.例如节点的宽高,相对包含块的位置.
大部分时候,DOM树和布局树并非一一对应.
比如display:node
的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器,虽然DOM树中不存在这些伪元素节点,但他们拥有几何信息,所以会生成到布局树中.还有匿名行盒,匿名块盒等等都会导致DOM树和布局树无法一一对应.
第四步是分层
主线程会使用一套复杂的策略对整个布局树进行分层
分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率.
滚动条,堆叠上下文,transform, will-change,opacity等样式或多或少的影响分层结果,也可以通过will-change
属性更大程度的影响分层结果.
第五步是绘制
主线程会为每个层单独产生绘制指令集,用于面熟这一层的内容该如何画出来.
第六步是分块
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成.
合成线程首先对每个图层进行分块,将其划分为更多的小区域.
他会从线程池中拿取多个线程来完成分块工作.
第七步是光栅化
合成线程会将块信息交给GPU进程,以极高的速度完成光栅化.
GPU进程会开启多个线程来完成光栅化,并且优先处理靠近视口区域的块.
光栅化的结果,就是一块一块的位图.
最后一个阶段就是画
合成线程拿到每个层,每个块的位图后,生成一个个指引(quad
)信息.
指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转,缩放等变形.
变形发生在合成线程,如渲染主线程无关,这就是transform
效率高的本质原因.
合成线程会把指引信息(quad
)交给GPU进程,由GPU进程产生系统调用,提交给GPU硬件,完成最终的屏幕成像.
什么是reflow
reflow的本质就是重新计算layout
树.
当进行了会影响布局树的操作后,需要重新计算布局树,会引发layout
为了避免连续的多次操作导致布局树的反复计算,浏览器会合并这些操作,当JS代码全部完成后再进行统一计算.所以,改动属性造成的reflow是异步完成的.
也同样如此,当JS获取布局属性时,就可能造成无法获取到最新的布局信息.
浏览器在反复权衡之下,最终决定获取属性立即reflow
(读取布局属性会立即添加一个同步任务)
什么是repaint
repaint
的本质就是重新根据分层信息计算了绘制指令.
当改动了可见样式后,就需要重新计算,会引发repaint
由于元素的布局信息也属于可见样式,所以reflow
一定会引起repaint
为什么transform效率高
因为transform几不会影响布局,也不会影响绘制指令,它影响的只是渲染流程的最后一个draw
阶段
由于draw
阶段在合成线程中,所以transform
的变化几乎不会影响渲染主线程.反之,渲染主线程无论如何忙碌,也不会影响transform
的变化.