概要
- 浏览器架构总览
- 进程、线程
- 站点隔离
- 渲染流程总览
- 导航阶段
- UI进程?拼装URL
- 网络进程?获取数据
- 重定向
- 根据Content-Type进行数据处理
- 唤起渲染进程
- 更新Tab状态 -->导航阶段结束
- 渲染阶段
- 编译处理
- BNF
- HTML 解析器
- DOM
- 标记算法
- DOM树构建算法
- 处理子资源
- DOM
- 将CSS附加(attachment)到DOM节点==>生成Render Tree
- CSS 解析器
- 创建呈现器
- 属性标准化
- 样式计算并保存到ComputedStyle
- 布局(Layout)
- 绘制(Paint)-生成元素绘制顺序
- 页面合成(Compositing)-->先分层,栅格化->页面合成
- 分层
- 栅格化(raster)操作
- 导航阶段
前提概要
在今年第一篇文章中,我们已经讲述过,我们以后的行文路线会按照前端 Roadmap
的进行。
我们今天来简单介绍一下行文路径中 Browser and how they work - 浏览器是如何运行的
。
根据权威机构统计调查,常规的主流浏览器在全球范围内所在的比重如下图所示。
所以,本文以Chrome
浏览器来展示浏览器的工作流程。
浏览器架构总览
进程、线程
在开始介绍浏览器工作流程的时候,我们需要简单说一下,进程
、线程
。
当你启动一个应用程序,对应的进程就被创建。进程可能会创建一些线程用于帮助它完成部分工作,新建线程是一个可选操作。在启动某个进程的同时,操作系统(OS)也会分配内存以用于进程进行私有数据的存储。该内存空间是和其他进程是互不干扰的。
当应用程序被关闭,进程也随之关闭,同时OS会将进程所占的内存释放掉。
其实这是一个动图,由于掘金没法嵌入 svg
格式的图片,video
也不行。所以,如果想看流程可以通过 传送门进行了解。
有人的地方就会有江湖,如果想让多人齐心协力的办好一件事,就需要一个人去统筹这些工作,然后通过大喇叭将每个人的诉求告诉对方。而对于计算机而言,统筹的工作归OS系统负责,OS通过Inter Process Communication (IPC)
的机制去传递消息。
其实这是一个动图,由于掘金没法嵌入 svg
格式的图片,video
也不行。所以,如果想看流程可以通过 传送门进行了解。
下面,我们来看看Chrome架构是如何组织的。
站点隔离
我们之前介绍浏览器渲染进程 -> 一般情况下,一个Tab页对应一个渲染进程。这里存在一个漏洞,页面中存在跨域的 iframe
,该 iframe
就会有访问该渲染进程内存的权限,这就违背了,同源策略。 所以,在最新的chrome架构中,如果页面中存在跨域的 iframe
,这些跨域的 iframe
也会唤起一个属于自己的渲染进程。
渲染流程总览
虽然,该篇文章以 chrome浏览器来解释 浏览器的工作流程,但是我们还是需要对其他浏览器的渲染进程
配置做一个汇总。
导航阶段
UI进程?拼装URL
当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字是搜索内容,还是请求的 URL。
- 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来
合成
新的带搜索关键字的 URL。 - 如果判断输入内容符合 URL 规则,那么地址栏会根据规则,把内容加上协议,合成为完整的 URL。
网络进程?获取数据
当最终的URL拼装好后,会由UI进程通过IPC
通知浏览器进程,而浏览器进程会将消息传递给网络进程。(此时的浏览器进程充当一个消息中转站的角色) 当网络进程接受到来自UI进程需要网络连接的消息后。随之会初始化一个网络连接。
首先,网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。这请求前的第一步
是要进行 DNS 解析,以获取请求域名的服务器 IP 地址。如果请求协议是 HTTPS,那么还需要建立 TLS 连接。
接下来就是利用 IP 地址和服务器建立 TCP 连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。
服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。
重定向
在接收到服务器返回的响应头后,网络进程开始解析响应头,如果发现返回的状态码是 301
(永久性的移动) 或者 302
(临时性重定向),那么说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location
字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始了。
在导航过程中,如果服务器响应行的状态码包含了 301、302 一类的跳转信息,浏览器会跳转到新的地址继续导航;如果响应行是 200,那么表示浏览器可以继续处理该请求。
根据Content-Type进行数据处理
不同 Content-Type 的后续处理流程也截然不同。如果 Content-Type 字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是Content-Type:text/html
,那么浏览器则会继续进行导航流程。
唤起渲染进程
由于,浏览器和服务器之间数据的交互时间存在不确定性,在响应体回来以后,再唤起渲染进程是存在滞后的。
所以在UI进程向网络进程发送拼装好的URL的时候,已经知道后续的导航的信息。此时UI进程会尝试随机唤起一个渲染进程。以备在响应体数据满足渲染要求,直接进行渲染操作。
更新Tab状态 -->导航阶段结束
在响应数据和渲染进程准备就绪后,网络进程通过IPC向渲染进程传递提交响应体数据的消息。
- 渲染进程和网络进程会建立数据通道。网络进程的数据就会源源不断的流向渲染进程。
- 当数据都传送完毕后,渲染进程会向UI进程发送确认提交的消息。
- UI进程在接收到确认提交消息,更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
渲染阶段
下面我们来重点讲述下,Chrome浏览器是如何将 HTML
和 CSS
拼装成浏览器可识别的信息。
想必大家都见过这张图,该图显示的是 WebKit
内核的渲染页面主流程。之所以,我把这个拿出来,是因为 Chrome
/Safari
/Edge
的渲染引擎都是 基于 Webkit 改造而来的。所以,我们通过了解 Webkit 的渲染流程,就能通晓市面上大部分浏览器的运行流程。
而上面所展示的图,是 2011 年所绘制的,现在都 1202
年了,有些流程和细节有变更和填充。但是,大体的流程和处理方式是一脉相承的。
我们通过导航阶段,从服务器中获取到用于浏览器展示的数据信息-- HTML
/CSS
。但是,HTML和CSS都是文本信息,是无法被浏览器所识别和使用,所以就需要一个机制,让HTML等文本信息转换为浏览器能够识别的格式,而这个转换过程就是解析阶段。
编译器
大多数编译程序(compiler)分为三个步骤:Parsing(分析阶段)/Transformation(转换)/Code Generation(代码生成或者说生成目标代码)
- Parsing将源代码(raw code)通过词法分析和语法分析转换为AST(抽象语法树)。
- Transformation接收Parsing生成的AST,并且按照compiler内定的规则进行代码的转换。
- Code Generation 接受被compiler转换过的代码,按照一定的规则将代码转换为最终想要输出的代码格式
通过以上三个步骤,大部分程序会被编译为目标代码。如果想了解更多关于编辑器是如何运行的,可以参考我原来写的编译程序(compiler)的简单分析。 在这里就不多啰嗦了。
BNF
常规的与上下文无关的语言,是可以通过 BNF
格式来描述。
例如,我们在解析 2 + 3 - 1
这个表达式时,
词法规则,我们可以用:
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
语法规则:
expression ::= term operation term
operation ::= PLUS | MINUS
term ::= INTEGER | expression
生成的 AST
结构
通过对AST进行个性化处理,最终生成指定机器和引擎能够识别的机器语言。
在讲述BNF是啥的时候,我们提到了 与上下文无关
这个概念。根据 维基百科的描述,
HTML 解析器
既然,我们说HTML想要被浏览器识别,是需要被解析的,而由于HTML的语言特性或者独特的解析过程,HTML是不能使用常规 上下文无法的编译器进行转换的。
理由如下:
- 语言的宽容本质
- 浏览器历来对一些常见的
无效
HTML 用法采取包容态度 - 解析过程需要不断地反复。源内容在解析过程中通常不会改变,但是在 HTML 中,脚本标记如果包含 document.write,就会添加额外的标记,这样解析过程实际上就更改了输入内容
DOM
而HTML解析器最终的目标就是为了,将HTML转换为浏览器能识别的数据结构 -- DOM
。
由于不能使用常规的解析技术,浏览器就创建了自定义的解析器来解析 HTML。
此算法由两个阶段组成:标记化和树构建。
标记算法
标记化是词法分析过程,将输入内容解析成多个标记(tokens
)。HTML 标记包括起始标记、结束标记、属性名称和属性值。
DOM树构建算法
标记生成器识别标记,传递给树构造器,然后接受下一个字符以识别下一个标记;如此反复直到输入的结束。
处理子资源
网站中总是会嵌入图片
、CSS
、JS
等非文本资源,而这些非文本信息需要再次从服务器或者缓存中获取。在DOM树构建过程中,如果遇到此类HTML标签,主线程将会依次请求对应的数据信息。而为了加快构建速度,预加载扫描器会和构建DOM树同步运行。 在标记化过程中,如果遇到类似<img>
或者<link>
标签,预加载扫描器会通知网络进程发起获取对应标签数据信息的异步请求。(此时主线程和网络请求是同步的)
一切都归于完美,但是如果非文本标签是是<script>
,就是另外一回事了。当标记算法输出的是<script>
,此时HTML的解析过程就会停止,也就是说,主线程不会在继续解析tokens,转而去加载、解析、执行对应的JS代码。只有在JS代码执行完成以后,HTML的解析才会继续进行。
由于<script>
会阻塞主线程构建DOM树,所以如果<script>
中不存在document.write()
这种对已构建DOM树毁灭性打击的行为,我们可以通过对script
设置defer
/async
属性来避免阻塞。
<script async src="A.js"></script>
有 async
,加载和渲染后续文档元素的过程将和 A.js 的加载与执行并行
进行(异步)。
<script defer src="B.js"></script>
有 defer
,加载后续文档元素的过程将和 B.js 的加载并行进行(异步),但是 B.js 的执行要在所有元素解析完成之后,DOMContentLoaded
事件触发之前完成。
从实用角度来说呢,首先把所有脚本都丢到 </body>
之前是最佳实践,因为对于旧浏览器来说这是唯一
的优化选择,此法可保证非脚本的其他一切元素能够以最快的速度得到加载和解析。
接着,我们来看一张图咯:
蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。
此图告诉我们以下几个要点:
defer
和 async
在网络读取(下载)这块儿是一样的,都是异步的(相较于 HTML 解析)
它俩的差别在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的
同时,我们可以通过<link preload/>
对资源进行预加载处理。
具体如何使用,可以参考 通过link的preload进行内容预加载,描述的很详细。或者对性能优化感兴趣,可以参考外文 Fast load times(后续有计划做整理和翻译,敬请期待)
经过一顿操作,HTM解析器终于将HTML转化为浏览器能够识别的 DOM
结构。
通过对百度首页渲染流程来简单看一下。
将CSS附加(attachment)到DOM节点==>生成Render Tree
解析样式和创建呈现器的过程称为“附加
”。每个 DOM 节点都有一个“attach
”方法。附加是同步进行的,将节点插入 DOM 树需要调用新的节点“attach”方法。
处理 html
和 body
标记就会构建呈现树根节点。这个根节点呈现对象对应于 CSS 规范中所说的容器 block
,这是最上层的 block,包含了其他所有 block。它的尺寸就是视口,即浏览器窗口显示区域的尺寸。 WebKit 称之为 RenderView
。这就是文档所指向的呈现对象。呈现树的其余部分以 DOM 树节点插入的形式来构建。
CSS 解析器
由于HTML解析和CSS解析都是在渲染进程中,并且渲染进程只存在一个主线程,也就意味着主线程在同一时间只能做一件事。-->单线程特性。
然后在DOM构建完成,并且将位置靠前的<script>
也处理完,以后才会开始CSS的解析步骤。
由于CSS
是上下文无关语言,所以解析CSS可以使用常规的编译器。而W3C中CSS定义了相关的词法和语法。
解析器会将 CSS
文件解析成 StyleSheet
对象,且每个对象都包含 CSS 规则。CSS 规则对象则包含选择器和声明对象,以及其他与 CSS 语法对应的对象。
创建呈现器
这是由可视化元素
按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是按照正确的顺序绘制内容。
class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node; //the DOM node
RenderStyle* style; // the computed style
RenderLayer* containgLayer; //the containing z-index layer
}
每一个呈现器都代表了一个矩形的区域,通常对应于相关节点的 CSS 框,包含诸如宽度、高度和位置等几何信息。
框的类型会受到与节点相关的“display
”样式属性的影响。例如,针对 display:block
的元素,它矩形区域默认独占一行,而 display:inline
的元素,是具有包裹性。(其实,针对CSS中盒模型是一个很大的课题,这块可以参考张鑫旭大佬有关的讲解。同时,自己也会有一定的文档说明,最近在做总结和梳理,敬请期待!)
属性标准化
现在我们已经将 CSS 节点解析为 RenderObject
,但是在写CSS的时候,我们会写诸如font-size:2em
的条件,而这些em是相对值,不是一个定值,所以,我们就需要将如 2em
、blue
、bold
,这些不容易被渲染引擎理解的值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。
如果存在如下的样式信息
body { font-size: 2em }
p {color:blue;}
span {display: none}
div {font-weight: bold}
div p {color:green;}
div {color:red; }
样式计算并保存到ComputedStyle
在样式标准化后,渲染引擎已经可以识别每个 RenderObject
中所携带真正的数据信息了, 但是DOM 节点和 RenderObject
可能存在一对多的关系。所以,我们需要将这些信息进行融合,这样才可以将最后的样式信息作用到 DOM 节点上。
某个样式属性的声明可能会出现在多个样式表中,也可能在同一个样式表中出现多次。这意味着应用规则的顺序极为重要。这称为“层叠”顺序。根据 CSS3 规范,层叠的顺序为(优先级从高到低,降序排序): 我们可以简化一下,就是
- CSS3的
transition
--> 优先级最高 - 浏览器重要声明 --> user agent stylesheet 中存在
!important
- 用户重要声明 --> 用户,就是直接在浏览器写的带有
!important
的属性 - 作者重要声明 -->
<link>
/<style>
/style
属性中带有!important
的属性 - 动画属性
- 作者普通声明 -->
<link>
/<style>
/style
属性 - 用户普通声明 --> 用户设置的自定义样式
- 浏览器声明 -->
user agent stylesheet
浏览器默认属性
相关连接请参考 www.w3.org 和 css-cascade-4。
同时,如果不同样式都作用于同一 DOM节点,就需要有一个权重计算的规则。
一图胜千言,有木有。
我们通过权重计算等操作,最后可以确定了针对指定DOM 节点所携带的在最终样式信息,而这些信息会被存到 ComputedStyle
结构中。
如果在实际开发中,你用过style = window.getComputedStyle(element);
该方法,返回了指定element所有的属性。而这个的方法返回的数据信息,其实就是通过一系列计算得到的 ComputedStyle
结构。
最后,我们得到了,一棵通过向 DOM树 附加样式信息的 Render Tree
。
布局(Layout)
通过HTML 解析和 CSS 解析,已经将HTML和CSS信息融合到一起,并且知道了每个节点各自的外观样式信息。但是光有外观样式信息还是不能够将节点排布到他们真正需要渲染到页面中的位置。还需要该元素对应的位置和大小信息。
呈现器在创建完成并添加到呈现树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排。
HTML 采用基于流的布局模型,处于流中靠后位置元素通常不会影响靠前位置元素的几何特征,因此布局可以按从左至右、从上至下
的顺序遍历文档。
布局是一个递归的过程。它从根呈现器(对应于 HTML 文档的 元素)开始,然后递归遍历部分或所有的框架层次结构,为每一个需要计算的呈现器计算几何信息。
根呈现器的位置左边是 0,0,
其尺寸为视口(也就是浏览器窗口的可见区域)。
所有的呈现器都有一个“layout
”或者“reflow
”方法,每一个呈现器都会调用其需要进行布局的子代的 layout 方法。
布局是一个寻找元素几何形状的过程。主线程遍历DOM和计算样式并创建布局树,布局树包含诸如x y坐标和边框大小等信息。呈现器是和 DOM 元素相对应的,但并非一一对应。非可视化的 DOM 元素不会插入布局树中,例如“head
”元素。如果元素的 display
属性值为“none
”,那么也不会显示在呈现树中(但是 visibility 属性值为“hidden
”的元素仍会显示)。
主线程遍历Render Tree 然后生成Layout Tree (布局树)
绘制(Paint)-生成元素绘制顺序
通过,布局处理,已经知晓所以元素的大小,位置。但是,还是不能进行按部就班的进行页面渲染,虽然HTML 采用基于流(从左到右,从上到下
)的布局模型进行布局,但是通过样式,可以脱离了流的默认流向和渲染顺序。
例如我们可以通过z-index
将在Z-轴方向搞点事情,这里就涉及到一个新的概念--层叠上下文 (这玩意也是一个很大的课题,如果有兴趣了解的话,还是可以参考张鑫旭大佬写的深入理解CSS中的层叠上下文和层叠顺序 )
直接上图,具体实现和讲解,就先不讨论了。
所以渲染器就从布局树的根节点进行遍历,按照各个维度进行最后的渲染顺序的确认,并生成元素的绘制顺序(paint record)。
页面合成(Compositing)-->先分层,栅格化->页面合成
页面合成是一种技术,将页面的各个部分分离成层,分别栅格化它们,并在称为复合线程
的单独线程中复合为页面。如果发生滚动,因为图层已经栅格化了,它所要做的就是合成一个新帧。动画也可以通过移动图层和合成新帧来实现。
分层
现在我们已经知道了,元素之间的绘制顺序,此时如果一股脑的从根节点开始渲染,将会是一项很大的工程,所以渲染引擎为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree
)--> 分而治之
完成了图层树的分割,主线程就开始遍历图层,并且生成一系列渲染记录 -> 用于指示渲染引擎对该图层的渲染顺序。
栅格化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程
来完成的。
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport
)
在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
基于这个原因,合成线程会将图层划分为图块(tile
),这些图块的大小通常是 256x256 或者 512x512
在处理完一帧的数据以后,就会向合成线程就会通过IPC将处理好的数据,返回给浏览器进程用于显示页面。而不占用渲染进程的主进程。
如此往复,直到合成线程中栅格化线程池中数据都被消费掉,页面也就渲染好了。
备注:该篇文章参考了很多资料,算是一个大杂烩。如果大家有兴趣,想看原文,可以直接参考。
- How Browsers Work: Behind the scenes of modern web browsers
- Inside look at modern web browser
- w3c
- 极客时间的课程
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!