简介
浏览器的主要功能
浏览器的主要功能就是向服务器发出请求,在浏览器窗口中展示您选择的网络资源。这里所说的资源一般是指 HTML 文档,也可以是 PDF、图片或其他的类型。资源的位置由用户使用 URI(统一资源标示符)指定。浏览器解释并显示 HTML 文件的方式是在 HTML 和 CSS 规范中指定的。这些规范由网络标准化组织 W3C(万维网联盟)进行维护。
浏览器的高层结构
- 用户界面: 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
- 浏览器引擎: 在用户界面和呈现引擎之间传送指令。
- 呈现引擎: 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- 网络: 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
- 用户界面后端: 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
- JavaScript 解释器: 用于解析和执行 JavaScript 代码。
- 数据存储: 这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。
资源加载
网页加载过程
- 浏览器发送请求,加载HTML资源,并开始解析;
- 解析过程中,遇到link标签,浏览器发出对CSS文件的请求;
- 当浏览器解析到
<body>
标签,并且CSS文件已经加载完成,开始渲染页面; - 当浏览器遇到
<script>
标签,阻塞页面的解析以及其他资源的下载,直到JS文件加载执行完毕; - 浏览器完成解析HTML页面。
并行加载
图片、js、css都属于静态资源,可以并行下载。此外,浏览器并不是严格地按照顺序下载静态资源,它会根据优先级来安排下载顺序(JS优先级高于图片)。
目前,在http 1.1协议下,chrome等大多数浏览器的并行下载数为6.
JS两种异步加载形式
defer
:异步加载<script>
资源,加载完成并不立即执行,而是等待HTML DOM元素加载完成后执行;async
:异步加载<script>
资源,加载完成立即执行。
1 | <script src="x.js" defer="defer"></script> |
避免页面中出现多个
defer
与async
加载的脚本,特别是对依赖性较强的脚本。因为虽然依照标准同样是defer
的资源应按照先后顺序执行,但浏览器的实现中并不是都能满足这一点。
呈现引擎
引擎种类
Firefox 使用的是Gecko
,这是 Mozilla 公司“自制”的呈现引擎。而 Safari 和 Chrome 浏览器使用的都是WebKit
。WebKit 是一种开放源代码呈现引擎,起初用于 Linux 平台,随后由 Apple 公司进行修改,从而支持苹果机和 Windows。
主流程
- 呈现引擎一开始会从网络层获取请求文档的内容,内容的大小一般限制在 8000 个块以内;
- 呈现引擎将开始解析 HTML 文档,并将各标记逐个转化成“内容树”上的 DOM 节点;
- 同时解析外部 CSS 文件以及样式元素中的样式数据;
- 基于HTML中这些带有视觉指令的样式信息创建另一个树结构:呈现树;
- 呈现树包含多个带有视觉属性(如颜色和尺寸)的矩形,这些矩形的排列顺序就是它们将在屏幕上显示的顺序;
- 为每个节点分配一个应出现在屏幕上的确切坐标;
- 呈现引擎遍历呈现树,由用户界面后端层将每个节点绘制出来。
呈现流程是一个渐进过程。为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,呈现引擎会将部分内容解析并显示出来。
WebKit 主流程:
Mozilla 的 Gecko 呈现引擎主流程:
处理脚本与样式表
脚本
网络的模型是同步的。网页作者希望解析器遇到<script>
标记时立即解析并执行脚本。此时,文档的解析将停止,直到脚本执行完毕。如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续。作者也可以将脚本标注为“defer”,这样它就不会停止文档解析,而是等到解析结束才执行。HTML5 增加了一个选项,可将脚本标记为异步,以便由其他线程解析和执行。
预解析
WebKit 和 Firefox 都进行了这项优化。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。
样式表
样式表有着不同的模型。理论上来说,应用样式表不会更改 DOM 树,因此似乎没有必要等待样式表并停止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。如果当时还没有加载和解析样式,脚本就会获得错误的回复,这样显然会产生很多问题。这看上去是一个非典型案例,但事实上非常普遍。Firefox 在样式表加载和解析的过程中,会禁止所有脚本。而对于 WebKit 而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。
容错
不同的浏览器都对HTML的解析提供了良好的容错功能,当发现语法错误时,浏览器会有一套自己的容错处理机制纠正错误的部分。
呈现树
在 DOM 树构建的同时,浏览器还会构建另一个树结构:呈现树。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是让浏览器按照正确的顺序绘制内容。但其不具备布局与定位信息。
呈现树与DOM树的关系
呈现器是和 DOM 元素相对应的,但并非一一对应。非可视化的 DOM 元素不会插入呈现树中,例如“head”元素。如果元素的 display 属性值为“none”,那么也不会显示在呈现树中(但是 visibility 属性值为“hidden”的元素仍会显示)。
有一些 DOM 元素对应多个可视化对象。它们往往是具有复杂结构的元素,无法用单一的矩形来描述。例如,“select”元素有 3 个呈现器:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮。如果由于宽度不够,文本无法在一行中显示而分为多行,那么新的行也会作为新的呈现器而添加。
另一个关于多呈现器的例子是格式无效的 HTML。根据 CSS 规范,inline 元素只能包含 block 元素或 inline 元素中的一种。如果出现了混合内容,则应创建匿名的 block 呈现器,以包裹 inline 元素。
有一些呈现对象对应于 DOM 节点,但在树中所在的位置与 DOM 节点不同。浮动定位和绝对定位的元素就是这样,它们处于正常的流程之外,放置在树中的其他地方,并映射到真正的框架,而放在原位的是占位框架。
布局
呈现器在创建完成并添加到呈现树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排。
HTML布局
HTML 采用基于流式布局模型,这意味着大多数情况下只要一次遍历就能计算出几何信息。处于流中靠后位置元素通常不会影响靠前位置元素的几何特征,因此布局可以按从左至右、从上至下的顺序遍历文档。但是也有例外情况,比如 HTML 表格的计算就需要不止一次的遍历。
Dirty位系统
为避免对所有细小更改都进行整体布局,浏览器采用了一种“dirty 位”系统。如果某个呈现器发生了更改,或者将自身及其子代标注为“dirty”,则需要进行布局。
有两种标记:“dirty”和“children are dirty”。“children are dirty”表示尽管呈现器自身没有变化,但它至少有一个子代需要布局。
全局布局与增量布局
- 全局布局
局布局是指触发了整个呈现树范围的布局,触发原因可能包括:
- 影响所有呈现器的全局样式更改,例如字体大小更改;
- 屏幕大小调整。
- 增量布局
只对 dirty 呈现器进行布局(这样可能存在需要进行额外布局的弊端)。当呈现器为 dirty 时,会异步触发增量布局。例如,当来自网络的额外内容添加到 DOM 树之后,新的呈现器附加到了呈现树中。
异步布局和同步布局
异步布局
增量布局是异步执行的。Firefox将增量布局的“reflow 命令”加入队列,而调度程序会触发这些命令的批量执行。WebKit 也有用于执行增量布局的计时器:对呈现树进行遍历,并对 dirty 呈现器进行布局。同步布局
全局布局往往是同步触发的。有时,当初始布局完成之后,如果一些属性(如滚动位置)发生变化,布局就会作为回调而触发。
布局优化
如果布局是由“大小调整”或呈现器的位置(而非大小)改变而触发的,那么可以从缓存中获取呈现器的大小,而无需重新计算。
在某些情况下,只有一个子树进行了修改,因此无需从根节点开始布局。这适用于在本地进行更改而不影响周围元素的情况,例如在文本字段中插入文本(否则每次键盘输入都将触发从根节点开始的布局)。
绘制
在绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。绘制工作是使用用户界面基础组件完成的。
全局绘制和增量绘制
和布局一样,绘制也分为全局(绘制整个呈现树)和增量两种:
- 全局绘制
即遍历整棵呈现树,从根节点到叶节点顺序绘制。 - 增量绘制
在增量绘制中,部分呈现器发生了更改,但是不会影响整个树。更改后的呈现器将其在屏幕上对应的矩形区域设为无效,这导致 OS 将其视为一块“dirty 区域”,并生成“paint”事件。
绘制顺序
绘制的顺序其实就是元素进入堆栈样式上下文的顺序:
- 背景颜色
- 背景图片
- 边框
- 子代
- 轮廓
重绘与重排
在发生变化时,浏览器会尽可能做出最小的响应。因此,元素的颜色改变后,只会对该元素进行重绘。元素的位置改变后,只会对该元素及其子元素(可能还有同级元素)进行布局和重绘。添加 DOM 节点后,会对该节点进行布局和重绘。一些重大变化(例如增大“html”元素的字体)会导致缓存无效,使得整个呈现树都会进行重新布局和绘制。
呈现引擎的线程
呈现引擎采用了单线程。几乎所有操作(除了网络操作)都是在单线程中进行的。
事件循环
浏览器的主线程是事件循环。它是一个无限循环,永远处于接受处理状态,并等待事件(如布局和绘制事件)发生,并进行处理。