浏览器主要功能
浏览器第一个主要功能就是从服务器下载Web资源并在浏览器窗口中将它呈现。这些资源可以是 HTML 文档,也可以是 PDF,图像等。而浏览器解析并显示 HTML 文档与如何处理 CSS 由 W3C组织 规范制定。以下是浏览器的组成要件:
- UI(用户界面):除了网页内容窗体以外的区域,包括地址栏、状态栏、工具栏、后退/前进按钮。
2.浏览器引擎:用户界面和呈现引擎之间传递指令。
呈现(渲染)引擎:负责解析并显示请求内容。
网络组件:负责网络请求,如 HTTP 请求。
UI 后端:负责绘制基本的小部件,如系统模态弹窗。
JavaScript 解析器:负责解析和执行 JavaScript 代码。
数据存储:浏览器需要保存在硬盘上的各种数据,如 Cookie、IndexedDB。
渲染引擎的基本流程
- 解析 HTML 文档构造 DOM 树。
- “渲染”过程将解析外部 CSS 文件和元素的样式属性,渲染树包含多个视觉效果并以正确的显示顺序的矩形。
- “布局”过程将每个节点定位在屏幕的确切坐标上。
- “绘画”过程遍历每个节点使用 UI后端涂漆(绘制)。
这是一个渐进的过程,为了更好的用户体验,渲染引擎会一边解析 HTML 一边开始构建和布局渲染,这个阶段一直保持与服务器通讯。
解析和 DOM 树构建
能把文档转换为有意义的结构并让计算机能够理解及使用,我们称之为解析,它必须遵循特定的语法规则并与上下文无关,解析的结果是具有文档结构的节点树,也叫作解析树或语法树。
解析器
解析可以分为两个子过程:词法分析和语法分析。
词法分析:
将输入分解成标记的过程,标记就是语言词汇(有效构建块的集合)。在人类语言中,相当于字典中的单词。语法分析:
应用语言语法规则的过程。
解析器就是负责将输入转换为有效标记,并根据语言的语法规则分析该文件结构并构造出解析树,词法分析会知道如何去除不相关的字符,比如空格和换行符。
有时候解析树并不是最终产品,通常还可以用于翻译,将输入文档转换为另一种格式。编译器会首先将源代码解析成解析树,然后将该树翻译成机器代码。
HTML 解析器
HTML 解析器的工作就是将 HTML 标记解析为解析树,HTML 的词汇及语法由 W3C组织 规范制定。HTML 是具有与上下文相关的语法组成,<u>常规解析器无法解析 HTML文档</u>。在与 XML 相比,两者也存在一些差异,HTML 语法“随意”、“松散”,XML 则“严谨”、“苛刻”,所以,<u> XML 解析器无法解析 HTML</u>。
HTML DTD
DTD 文档类型定义用来定义 SGML 家族的语法,而 HTML 是基于 SGML 的。DTD 中包含允许出现的元素、属性和层次结构的定义,<u>这也说明了 HTML 是与上下文有关的语言标记</u>。
DOM
DOM 文档对象模型提供了一套 HTML 文档元素与外部世界连接的标准接口,这些规范也由 W3C组织 制定,树的根节点就是“文档”对象。
HTML 解析算法
HTML 解析算法分为两个阶段:标记化和树构建。
标记化算法
标记化就是词法分析。该算法状态机来表示,每个状态接收输入流的一个或多个字符,并根据当前字符更新下一个状态。接收相同字符,对于正确的下一状态可能产生不同的结果。我们来看一个简单的例子:
<html>
<body>
Hello world!
</body>
</html>
初始状态是“数据状态”。当遇到“<”字符时,状态更改为“标记打开状态”。接收“h”字符会创建一个“起始标记”,状态更改为“标记名称状态”,每消费一个字符都会附加在这个令牌名称上,保持这个状态直到遇见“>”字符,创建了一个“html”标记并发送。
接着状态回到“数据状态”,“<body>”标记将按相同的步骤处理。到目前为止,一共有两个标记被发送。
继续回到“数据状态”,接收“Hello world”的“H”字符会创建一个“字符标记”,一共有11个“字符标记”被发送。
接收“</body>”的“<”字符,状态再次回到“标记打开状态”,当接收下一个输入“/”会创建“结束标记”,状态更改为“标记名称状态”,继续接收“body”中的每个字符,直到遇见“>”,然后将新的标记被发送,状态回到“数据状态”,“</html>”输入执行相同处理。
树构建算法
在这个阶段,对于每个标记与规范中定义的 DOM 元素相关,则将该标记追加到根节点为 Document 对象的 DOM 树上。该算法以状态机来表示。
<html>
<body>
Hello world!
</body>
</html>
对应标记化阶段的第一模式是“初始模式”,接收发送过来的 html 起始标记,状态更改为“** Html 之前**”模式,将创建一个 HTMLHtmlElement 元素,并追加到 Document 对象上。
状态更改为“** Head 之前**”,显然没有收到 head 起始标记而是 body 起始标记,将隐式创建一个 HTMLHeadElement元素,并追加到 Document 对象上。
body 起始标记被重新处理,状态从为“** Head 之后”更改为“ Body 之前”模式,创建一个 HTMLBodyElement元素,并追加到 Document 对象上,同时状态更改为“在 Body 里**”。
现在接收到“Hello world”一系列字符标记,第一个“H”将创建和插入到“文本”节点,其它字符将附加到该节点上。
body 结束标记接收到后,状态更改为“** Body 之后”模式,接着又将接收到 html 结束标记,促使状态更改为“ Html 之后**”模式。接收文件的结束标记后将结束解析。
浏览器此时会将文档状态标记为“交互式”,并开始解析处于“延迟”模式的脚本程序,这些脚本将在解析文档后被执行,然后文档状态设置为“完成”,并触发“加载”事件。
浏览器容错
你将不会在 HTML 页面上看到“语法无效”的错误,浏览器会自动修复无效内容并继续,这种错误处理机制在其它浏览器中的做法是非常一致的,它不属于标准的一部分,就像书签和后退/前进按钮,都是浏览器多年来总结的经验。
CSS解析器
与 HTML 不同,CSS 是一个上下文无关的语法。语法在 BNF 中描述如下:
规则集
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
选择器
: simple_selector [ combinator selector | S+ [ combinator selector ] ]
;
简单选择器
: element_name [ HASH | class | attrib | pseudo ]*
| [ HASH | class | attrib | pseudo ]+
;
类
: '.' IDENT
;
元素名
: IDENT | '*'
;
属性
: '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
[ IDENT | STRING ] S* ] ']'
;
伪
: ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
;
每个 CSS 样式表会被解析为一个 StyleSheet 对象,每个对象里包含 CSS 规则。CSS 规则对象包含选择器和声明对象以及 CSS 语法相对应的其它对象。
解析脚本和样式表的顺序
脚本
<u>网络的模型是同步的,当解析器执行<script>
标记时,会立刻解析并执行脚本。如果是外部脚本,文档会停止解析,直到资源被获取。</u>你也可以将脚本标记为“defer”,那么它就不会停止文档解析,并在解析后执行脚本程序。HTML5 添加了一个选项来标记脚本为异步,因此可以视为不同线程在处理,这种策略叫预解析。
样式表
从概念上看,样式表不会改变 DOM 树,因此没有理由等待它们并停止文档解析。但是,有一个情况,当脚本在文档解析阶段请求样式信息,如果样式没有加载并解析,那么就会照成很多问题。
当仍在加载和解析样式表时,Firefox 会阻止所有脚本,Webkit 则只在脚本试图访问某些可能受尚未加载样式表影响时才阻止脚本。
渲染树建设
当 DOM 树被构建后,浏览器会构造另外一棵树,即渲染树。<u>该树的目的是以正确的顺序绘制内容并呈现视觉元素给用户。</u>
渲染器
Firfox 渲染树中的元素称为“框架”,Webkit 则使用术语渲染器或渲染对象,目的是如何布局和绘制渲染树。
每个渲染器通常对应节点的 CSS 框的矩形区域,它包含几何信息有宽度、高度和位置。
框的类型会受到与节点相关的“display”样式属性影响。
Webkit RenderObject 基类定义如下:
class RenderObject{
virtual void layout();
virtual void paint(PaintInfo);
virtual void rect repaintRect();
Node* node; // DOM节点
RenderStyle* style; //计算的样式
RenderLayer* containgLayer; //包含z-index层
}
以下为Webkit 如何根据属性决定应该为 DOM 节点创建什么类型的渲染器。
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->document();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;
switch (style->display()) {
case NONE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
...
}
return o;
}
渲染树与 DOM 树的关系
1.渲染器对应 DOM 元素,但关系不是一对一的。
非可视 DOM 元素不会插入到渲染树中,如head
元素和 display:none
的元素。
2.有对应多个可视化对象的 DOM 元素
通常是具有不能由单个矩形描述的复杂结构的元素。如select
元素需要3个渲染器,一个用于显示区域,一个用于下拉列表, 一个用于按钮。另外多行文本也需要添加额外的渲染器。
3.不合理地嵌套标记会产生额外渲染器
W3C 组织规范规定内联元素必须包含块元素或内联元素,在混合文本嵌套混乱的情况下,会创建匿名块渲染器包含内联元素。
4.渲染器对应 DOM 节点,但不在树上的相同位置。
浮动和绝对定位的元素处于正常文档流之外,放置在树中的其它地方并映射到正确的框架,而放在原位上的是占位符。
样式计算
构建渲染树需要计算每个渲染对象的表现属性,这是通过计算每个元素的样式属性来完成的。
样式属性包含各种来源样式表,内联样式元素和 HTML 中表现属性(如“bgcolor”属性)。后者被翻译为匹配的CSS样式属性。
样式表的来源包含浏览器的默认样式表,网页作者和用户自定义样式表,例如,通过在“Firefox Profile”文件夹中放置样式表来完成的。
规则简化匹配
样式表解析完后,系统会根据选择器将 CSS 规则添加到某个哈希表中,包括 ID 表、Class 表、Tag 表。匹配原则就是先找出那些根据键提取的规则,然后再进行正确的匹配。
层叠匹配规则
层叠顺序优先级:
- 浏览器声明
- 用户普通声明
- 作者普通声明
- 作者重要声明
- 用户重要声明
特征性:
- “Style”属性,记为 1,否则记为 0(=a)
- 选择器 ID 属性的个数(=b)
- 选择器中其他属性和伪类的个数(=c)
- 选择器中元素名称和伪元素的个数(=d)
布局
当创建渲染器并将其添加到树中时,它并没有位置和大小,计算这些值的阶段称为布局或回流。
HTML 采用基于流的布局模型
这意味着大多数时候只要一次遍历就能计算出几何信息,之后出现的元素通常不会影响较早的元素几何,所以布局就是从左到右,从上到下顺序遍历文档的。坐标相对于根框架
根渲染器的位置是从顶部左侧坐标 0,0 开始,它的尺寸是视口(浏览器窗口的可视部分)。渲染器的方法
所有渲染器都有一个“布局”或“回流”方法,每个渲染器都会调用其需要进行布局的子代“布局”方法。
脏位系统
为了不对每个小的改变做一个完整的布局,浏览器使用“脏位”系统。目的就是将被改变或添加的渲染器将其自身和后代标记为“脏”,也就是需要布局。
全局和增量布局
1.全局布局
触发“全局布局”的原因主要是影响所有渲染器的全局样式被更改,如字体大小,或是屏幕调整大小。
2.增量布局
当渲染器“脏”时,触发(异步)增量布局。例如,额外内容来自网络并添加到 DOM 树后,新的渲染器被附加到渲染树。
布局过程
布局通常具有以下模式:
- 父渲染器确定其自身的宽度。
- 父渲染器一次处理子渲染器,并且:
- 放置子渲染器(设置其 x 和 y 坐标)。
- 如果需要调用子布局(它们是脏的,或者全局布局或一些其他原因) ,这将计算子渲染器的高度。
- 父级使用子级累加高度和边距和填充的高度来设置自己的高度 - 这将可供父渲染器的父级使用。
- 将其脏位设置为false。
Firefox使用“状态”对象(nsHTMLReflowState)作为布局参数(称为“回流”)。其中状态包括父级宽度。
Firefox布局的输出是一个“metrics”对象(nsHTMLReflowMetrics)。它将包含计算得出的渲染器高度。
宽度计算
渲染器的宽度是使用容器块的宽度,渲染器的样式“width”属性,边距和边框计算的。
例如以下div的宽度:
<div style =“width:30%”/>
将由Webkit计算如下(类RenderBox方法calcWidth):
- 容器宽度是容器availableWidth和0的最大值。这种情况下的availableWidth是contentWidth,计算公式为:
clientWidth() - paddingLeft() - paddingRight()
clientWidth和clientHeight表示对象的内部,不包括边框和滚动条。
- 元素width是“width”样式属性。它将通过计算容器宽度的百分比计算为绝对值。
- 然后添加水平边框和padding。
这是“首选宽度”的计算。然后将计算最小和最大宽度。
如果首选宽度高于最大宽度,将使用最大宽度。如果它低于最小宽度(最小不可破碎单位),则使用最小宽度。值会被缓存,以防需要布局但宽度不变的情况。
换行
当渲染器在布局过程中需要换行,会立即停止布局,并告知其父代需要换行。父代将会创建额外的渲染器并调用布局。
绘制
在绘制阶段使用 UI 基础结构组件遍历渲染树,并且调用渲染器“绘制”方法以在屏幕上显示它们的内容。
全局和增量
像布局一样,绘制也可以是全局(整个树被绘制)或增量。
在增量绘制中,一些渲染器改变但不影响整个渲染树。更改后的渲染器会使其在屏幕上的矩形无效。这使得OS将其看作“脏区域”并且生成“绘制”事件。OS 巧妙地将几个区域合并为一个。
在Chrome 中它更复杂,因为渲染器不在主进程中。Chrome 在一定程度上模拟了 OS 的行为。
展示层会监听这些事件,并将消息委托给渲染根节点。遍历树直到找到相关渲染器。它会重新绘制自己(通常包括其子代)。
绘制顺序
CSS2 定义了绘制的顺序。其实就是元素进入堆栈样式上下文的顺序。这个顺序影响绘制,因为堆栈是从后到前绘制的。
块渲染器的堆叠顺序是:
- background color
- background image
- border
- children
- outline
Firefox显示列表
Firefox浏览渲染树并为已绘制的矩形构建一个显示列表。它包含与矩形相关的渲染器以及正确的绘制顺序(渲染器的背景,边框等)。
这样,等到树需要重绘时,只需遍历一次渲染树(绘制所有背景,图片,边框等)。
Webkit矩形存储
重新绘制之前,Webkit 将旧矩形另存为一张位图。然后它只绘制新旧矩形之间的增量。
动态更改
浏览器会尝试做出响应更改的最小可能性。
- 对元素的颜色的更改仅影响元素的重绘。
- 元素位置更改,影响其子元素(可能还有兄弟元素)进行布局和重绘。
- 新增的 DOM 节点导致节点的布局和重绘。
一些重大变化,如增加“html”元素的字体大小,导致整个树的缓存无效,使得整个渲染树进行重新布局和绘制。
渲染引擎的线程
呈现引擎是单线程的。除了网络以外,几乎所有的操作都在同一单线程中进行。
网络操作可由多个并行线程同时执行(一般是 2 至 6个)。
事件循环
浏览器的主线程是一个事件的无限循环,永远处于接受处理状态,并且等待事件(如布局和绘制)。以下是主要事件循环的 Firefox 代码:
while(!mExiting) NS_ProcessNextEvent(thread);
CSS2 可视化模型
画布
“画布”是用来呈现格式化结构的空间,也就是供浏览器绘制内容的区域。
CSS框模型
针对文档树中的元素生成,根据可视化格式模型进行布局的矩形框。每个框都有一个内容区域(如文本、图片等),还有可选的周围填充、边框和边距。所有元素都有一个“display”属性,决定它们所对应生成的框类型。
定位方案
定位方案由“position”属性和“float”属性设置的。
- 普通:根据对象在文档的位置进行定位,该对象在渲染树和在 DOM 树上的位置相似,并根据其框类型和尺寸进行布局,值是“static”或“relative”。
- 浮动:对象先按普通进行布局,然后尽可能向左或向右布局。
- 绝对:对象在渲染树的位置和在 DOM 树上的位置不同,值是“absolute”或“fixed”。
指定位置使用:top、bottom、left、right。
框的布局方式是由以下因素决定的:
- 框类型
- 框尺寸
- 定位方案
- 外部信息,例如图片大小和屏幕大小
框类型
block 框
- 形成一个 block(矩形区域),在浏览器窗口中拥有其自己的矩形区域。
- 采用垂直格式排列。
inline 框
- 没有自己的 block,但是位于容器 block 内。
- 采用水平格式排列。
定位
- 相对定位:先按照普通文档流定位,然后再跟进偏移量进行移动。
- 浮动:浮动框移动到行的左边或右边。其它框会浮动在它的周围。
- 绝对和固定定位:元素不参与普通文档流,尺寸是相对于容器而言的,固定定位中容器是可视区域。
分层展示
由 CSS 属性 z-index 属性指定。z-index 属性的优先级越高,那么移动到根框所保持的堆栈中更靠前的位置。