1. 介绍
浏览器可能是最广泛使用的软件。本书将介绍浏览器的工作原理。我们将看到,当你在地址栏中输入google.com
直到你看到Google页面,这个过程都发生了什么。
1.1 本文将讨论的浏览器
现在有五种主流浏览器——Internet Explorer,Firefox,Safari,Chrome和Opera。本书会基于开源浏览器的例子——Firefox,Chrome以及Safari,Safari是部分开源的。
根据W3C Browser Statistics的统计数据,当前(2009年10月)Firefox,Safari和Chrome的总市场占有率接近60%。因此,可以说开源浏览器已经占据了浏览器市场的半壁江山。(译注:截至到2016年八月,Chrome占58.1%、Safari占12.7%以及Firefox占12.4%,三者总市场占有83.2%)
1.2 浏览器的主要功能
浏览器的主要功能是将你选择的Web资源呈现出来,通过从服务器请求资源,然后将它在浏览器窗口中显示。资源的格式通常是HTML,但也包括PDF、图片以及其他格式。用户通过URI(Uniform Resource Identifier,统一资源标识符)来定位资源,我们会在网络这章详细介绍。
HTML和CSS规范中规定了浏览器解释和呈现HTML文档的方式。这些规范由W3C(World Wide Web Consortium)组织进行维护,它是负责制定Web标准的组织。
HTML规范的当前版本是HTML4,HTML5还在指定中。CSS规范的当前版本是CSS2,CSS3也还在指定中。(译注:本文写作时间是2009年)
过去这些年浏览器厂商纷纷开发自己的扩展,只遵循一部分规范,这给Web开发者造成了严重的兼容性问题。现如今,大多数的浏览器或多或少遵循规范。
但是浏览器的用户界面有很多相同点,常见的用户界面元素包括:
- 地址栏,用于输入URI
- 前进按钮和后退按钮
- 书签选项
- 刷新按钮和停止按钮,用于刷新和停止加载当前文档
- 主页按钮,帮助你直达主页
奇怪的是,浏览器的用户界面并没有在任何正式的规范中指定,它只是各浏览器厂商多年的经验和相互模仿不断改进的结果。HTML5规范没有规定浏览器必须具有的UI元素,但是列出了一些常用的元素,包括地址栏、状态栏以及工具栏。很显然,有些浏览器有自己特有的功能,如:Firefox的下载管理器。在用户界面这一章我们会详细介绍。
1.3 浏览器的主要构成
浏览器的主要组件包括:
- 用户界面——包括地址栏、后退/前进按钮、书签菜单等,也就是你看到的除了用来显式你请求页面的主窗口之外的其余部分。
- 浏览器引擎——查询和操作渲染引擎的接口。
- 渲染引擎——用来显示请求的内容。例如:如果请求内容为HTML,它负责解析HTML和CSS,并将解析后的结果在屏幕上显示。
- 网络——用来完成网络调用,比如HTTP请求。它具有平台无关的接口,在不同平台实现不同。
- UI后端——用来绘制基本组件,例如:组合下拉框和窗口等。具有平台无关的通用接口,底层使用操作系统的用户接口实现。
- JavaScript解释器——用来解释和执行JavaScript代码。
-
数据存储——属于持久层。浏览器需要在硬盘上保存各种各样的数据,例如Cookies。HTML5规范中定义了
Web Database
技术,这是一种完整(且轻量)的浏览器端数据库。
值得注意的是,不同于大多数的浏览器,Chrome为每个Tab分配了一个单独的渲染引擎实例,每个Tab都是一个独立的进程。我会为每个组件独立一章,与你们详细讨论。
1.4 组件间通信
Firefox和Chrome都开发了一个特殊的通信基础设施,它们将在一个专门的章节中讨论。
2. 渲染引擎
渲染引擎的职责就是渲染,也就是在浏览器屏幕上显示请求的内容。
默认情况下,渲染引擎可以显示HTML、XML文档和图片。它可以借助插件(一种浏览器扩展)显示其他类型的数据。例如,使用PDF阅读器插件显示PDF文档。有专门的一章讲解插件及扩展,本章只关注渲染引擎的主要用途——显示CSS格式化之后的HTML和图片。
2.1 渲染引擎
我们所讨论的浏览器——Firefox、Chrome和Safari是基于两种渲染引擎构建的。Firefox使用Gecko——Mozilla自主研发的渲染引擎。Safari和Chrome都使用Webkit。
Webkit是一款开源的渲染引擎,它本来是为Linux平台研发的,后来被Apple修改移植到了Mac和Windows上。更多细节请参考https://webkit.org/。
2.2 主流程
渲染引擎首先通过网络层获取请求文档的内容,通常以8K分块的方式完成。取得内容之后,渲染引擎的基本流程如下:解析HTML以构建DOM树 -> 构建Render树 -> 布局Render树 -> 绘制Render树。
渲染引擎开始解析HTML文档,并将标签转为内容树中的DOM节点。接下来,它解析外部CSS文件和style
标签中的样式信息。这些样式信息和HTML中的可见指令将被用来构建另一棵树——Render树(渲染树)。
Render树由一些包含视觉属性(如颜色和大小)的矩形组成,它们将按照正确的顺序显示到屏幕上。
Render树构建好之后将会执行布局过程,这意味着它将确定每个节点在屏幕上的确切坐标。下一步就是绘制——遍历Render树并使用UI后端层绘制每个节点。
值得注意的是,这个过程是逐步完成的。为了更好的用户体验,渲染引擎会尽可能早的将内容呈现到屏幕上,并不会等到所有的HTML都解析完成之后再去构建和布局Render树。它是解析完一部分内容就显示一部分内容,同时从网络上下载剩余内容。
值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时进程还在从网络上下载其余内容。
2.3 主流程案例
从图3和图4中可以看出,尽管Webkit和Gecko使用的术语稍有不同,但主流程基本相同。
Gecko称可见的格式化元素组成的树为Frame Tree,每个元素都是一个Frame;而Webkit使用术语Render Tree来表示由Render Object组成的树。Webkit使用术语Layout表示元素的定位,而Gecko中称为Reflow。Webkit使用术语Attachment表示连接DOM节点和样式信息去构建Render树的过程。这里有个微小的非语义上的不同,Gecko在HTML和DOM树之间附加了一层,它被称为Content Sink,是制造DOM元素的工厂。下面将讨论流程中的各个阶段。
2.4 解析与构建DOM树
2.4.1 解析概述
既然解析是渲染引擎中一个非常重要的过程,我们将稍微深入地研究它。首先简要介绍下解析。
解析一个文档就是将其转换为具有一定意义的结构——某些代码能够理解和使用的东西。解析的结果通常是表示文档结构的节点树,它被称为解析树或语法树。
例如:解析2 + 3 - 1
这个表达式可能返回这样一棵树:
2.4.1.1 文法
解析基于文档遵循的语法规则——写入文档的语言或格式。每种能被解析的格式,必须具有词汇以及语法规则组成的特定的文法,称为上下文无关文法。人类语言不具有这种特性,因此不能被传统的解析技术所解析。
2.4.1.2 解析器与词法分析器
解析可以分为两个子过程——词法分析和语法分析。
词法分析就是将输入分解为符号,符号就是语言的词汇表——有效构建块的集合。在人类语言中,相当于这门语言字典中出现的所有单词。
语法分析是指对语言应用语法规则。
解析器通常将工作分配给两个组件——词法分析器(有时也叫分词器)负责将输入分解为合法的符号,解析器则根据语言的语法规则分析文档结构,从而构建解析树。词法分析器知道如何去掉无关字符(如空格和换行符)。
解析过程是迭代的。解析器总是会从词法分析器那取一个新的符号,并试着用这个符号匹配一条语法规则。如果匹配了一条规则,这个符号对应的节点将被添加到解析树上,然后解析器会继续请求下一个符号。如果没有匹配到规则,解析器会在内部保存该符号,并从词法分析器取下一个符号,直到所有内部保存的符号能够匹配一条语法规则。如果最终没有找到匹配的规则,解析器将抛出一个异常,这意味着文档是无效的或者包含语法错误。
2.4.1.3 转换
很多时候解析树并不是最终产品。解析一般在转换中使用——将输入文档转换成另一种格式。编译就是个例子,编译器将源代码编译成机器码的时候,先将源代码解析为解析树,然后将该树转换成机器码文档。
2.4.1.4 解析实例
在图5中,我们从一个数学表达式构建了一棵解析树。我们在这里定义一个简单的数学语言来分析下解析过程。
词汇表:我们的语言包括整数、加号以及减号。
语法:
- 该语言的语法基本单元包括表达式、terms以及操作符。
- 该语言可以包括多个表达式。
- 一个表达式定义为两个term通过一个操作符连接。
- 操作符可以是加号或减号。
- 一个term可以是一个整数或一个表达式。
现在来分析下2 + 3 - 1
这个输入。第一个匹配规则的子串是2
,根据规则5,它是一个term。第二个匹配的是2 + 3
,它符合规则3——一个表达式定义为两个term通过一个操作符连接。下一次匹配发生在输入的结尾处。2 + 3 - 1
是一个表达式,因为我们已经知道2 + 3
是一个term,所以我们有了一个term紧跟着一个操作符再紧跟着另一个term。2 + +
不会匹配任何规则,因此是一个无效输入。
2.4.1.5 词汇和语法的形式定义
词汇表通常用正则表达式来定义。例如上面的语言可以定义为:
INTEGER: 0|[1-9][1-9]*
PLUS: +
MINUS: -
如你所见,这里用正则表达式定义整数。
语法通常用BNF来定义,上面的语言可以定义为:
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression
我们上面提到过,如果一个语言的文法是上下文无关的,则可以用正则解析器来解析。对上下文无关的文法的一个直观定义是,该文法可以用BNF完整的表达。正式定义请参考维基百科词条Context-free grammar。
2.4.1.6 解析器类型
有两种基本的解析器——自顶向下的解析器和自底向上的解析器。一个比较直观的解释是:自顶向下解析器查看语法的最高层结构,并试图匹配其中一个;自底向上解析器则从输入开始,逐步将其转换为语法规则,从底层规则开始直到匹配高层规则。
来看一下这两种解析器如何解析上面的例子:
自顶向下解析器从最高层规则开始,它会先识别出2 + 3
,将其视为一个表达式;然后识别出2 + 3 - 1
是一个表达式(识别表达式的过程中匹配了其他规则,但是起点是最高层规则)。
自底向上解析器会扫描输入,直到匹配了一条规则,然后用该规则替换匹配的输入,直到解析完所有输入。部分匹配的表达式被放置在解析堆栈中。
Stack | Input |
---|---|
2 + 3 - 1 | |
term | + 3 - 1 |
term operation | 3 - 1 |
expression | - 1 |
expression operation | 1 |
expression |
2.4.1.7 自动生成解析器
有工具可以自动生成解析器,它们被称为解析器生成器。你只需要指定语言的文法——词汇表和语法规则,它就可以生成一个解析器。创建一个解析器需要对解析有深入的理解,而且手动创建一个较好性能的解析器并不容易,所以解析器生成器非常有用。
Webkit使用两个著名的解析器生成器——用于创建词法分析器的Flex和创建解析器的Bison(你可能接触过Lex和Yacc)。Flex的输入是一个包含符号定义的正则表达式文件,Bison的输入是用BNF格式定义的语法规则。
2.4.2 HTML解析器
HTML解析器的工作是将HTML标签解析为解析树。
2.4.2.1 HTML文法定义
W3C组织指定了规范,定义了HTML的词汇表和语法。
2.4.2.2 非上下文无关的文法
我们在“解析介绍”中提到过,上下文无关的文法可以用类似BNF的格式来定义。
不幸的是,所有的传统解析方式都不适用于HTML(我提出它们并不是因为好玩,它们将用来解析CSS和JavaScript),HTML不能简单地用解析所需的上下文无关文法来定义。HTML有一个正式的格式定义——DTD(Document Type Definition)——但它并不是上下文无关的文法。
HTML更接近于XML,下载有很多可用的XML解析器,HTML有个XML版本的变种——XHTML,那二者之间最大的不同是什么?不同之处在于HTML更加“宽容”,它允许你忽略一些特定的标签,有时可以省略开始或结束标签。总体来说,它是一种柔软的语法,不同于XML呆板固执的语法。
很显然,这个看起来很小的差异却带来了很大的不同。一方面,这就是导致HTML流行的原因——它对错误的宽容,使得Web开发者工作更轻松;但另一方面,这使得要写一个格式化的文法变得更加困难。所以,总结一下,解析HTML并不简单,它既不能用传统的解析器解析,因为它不是上下文无关的文法,也不能用XML解析器解析。
2.4.2.3 HTML DTD
HTML是用DTD格式进行定义的,这种格式被用于定义SGML家族的语言。这种格式包括了对所有允许的元素、它们的属性以及层次关系的定义。正如前面提到的,HTML DTD不会生成一种上下文无关的文法。
DTD有一些变种,严格模式完全遵守规范,但其他模式包含对过去浏览器所使用标签的支持,这么做是为了兼容以前的内容。最新的严格DTD在此:strict.dtd。
2.4.2.4 DOM
输出的树也是解析树,由DOM元素和属性节点组成。DOM是Document Object Model (文档对象模型)的缩写,它既是HTML文档的对象表示,也是HTML元素对外部的接口供JavaScript等调用。树的根是“Document”对象。
DOM和标签基本是一一对应的关系。如下的标签:
<html>
<body>
<p>Hello World</p>
<div><x:img src="example.png" /></div>
</body>
</html>
<!--译注:请把x:img去掉x:,因为单独使用img标签。简书markdown会把它当作图片上传到服务器,为了写作方便,才加上x:img的-->
将会被转换为下面的DOM树:
和HTML一样,DOM规范也是由W3C组织制定的,这是操作文档的一般规范,一个特定的模块描述一种特定的HTML元素。HTML的定义请查阅这里:idl-definitions.html。
当我说树包含了DOM节点,我的意思是说该树是由实现了DOM接口的元素构建而成的,浏览器使用已被浏览器内部使用的其他属性的具体实现。
2.4.2.5 解析算法
正如前面章节中所讨论的,HTML不能被一般的自顶向下或自底向上的解析器所解析。原因如下:
- 语言本身的宽容性。
- 浏览器对一些常见的无效HTML有容错支持。
- 解析过程是往返的。通常情况下,源码不会在解析过程中发生改变,但在HTML中,脚本标签包含的
document.write
可能添加额外标签,所以在解析过程中实际上修改了输入。
不能使用正则解析技术,浏览器为了解析HTML,创建了专属的解析器。
HTML5规范中详细描述了这个解析算法,它由两个阶段组成——符号化和构建树。符号化阶段进行词法分析,将输入解析为符号。HTML符号包括开始标签、结束标签、属性名以及属性值。符号识别器识别出符号后,会将它传给树构建器,并读取下一个字符以识别下一个符号,循环此过程,直到处理完所有的输入。
2.4.2.6 符号识别算法
符号识别算法的输出是HTML符号,该算法用状态机表示。每个状态读取输入流中的一个或多个字符,并根据这些字符转移到下个状态。当前符号的状态以及构建树的状态共同影响结果,这意味着读取同样的字符,可能因为当前状态的不同,会得到不同的结果,以进入下个正确的状态。这个算法太复杂以至于不能讲解透彻,这里用一个简单的例子来帮助我们理解原理。
基本示例——符号化下面的HTML:
<html>
<body>
Hello World
</body>
</html>
初始状态是“Data State”,当遇到“<”字符,状态转变为“Tag Open State”,读取一个“a-z”的字符会产生一个开始标签符号,状态相应地转变为“Tag Name State”,一直保持这个状态,直到读取到“>”字符,每个字符都会追加到这个符号名上,在本例中,我们创建了一个符号“html”。
当读取到“>”字符,当前的符号就处理完了,此时状态就切回“Data State”了,“<body>”标签会重复这一处理过程。到这里“html”和“body”标签都识别出来了。现在我们又回到“Data State”,读取字符“H”将创建并识别出一个字符符号,我们会为“Hello World”中的每个字符生成一个字符符号,直到遇到“</body>”中的“<”。
现在我们又回到了“Tag Open State”,读取下个输入字符“/”将创建一个“闭合标签符号”,并且状态转移到“Tag Name State”,再一次保持这个状态直到遇到“>”。然后会产生一个新的标签符号,并回到“Data State”。“</html>”标签的处理跟前面一样。
2.4.2.7 构建树算法
当解析开始时文档对象也创建了,在树的构建阶段,以Document为根的DOM将被修改,元素会被添加到树上。每个被符号识别器识别出的节点都会被树构造器处理,规范中定义了每个符号相对应的DOM元素,该符号对应的DOM元素会被创建出来。除了将元素添加到DOM树上,还会将它添加到开发元素堆栈中。这个堆栈是用来纠正嵌套未匹配和未闭合的标签。这个算法也是用状态机来描述,所有的状态采用“插入模式”。
对于这个示例输入,我们来分析下它的树构造过程:
<html>
<body>
Hello World
</body>
</html>
构建树这一阶段的输入是符号识别阶段生成的符号序列。初始化模式是“initial mode”,接收到html符号将转移到“before html”模式,在这个模式中会对这个符号进行再处理。这会创建一个HTMLHtmlElement元素并且将它附加到根元素Document对象上。
当接收到body符号时,状态会转移到“before head”,即使这里没有head符号,也会隐式创建一个HTMLHeadElement元素并添加到树上。我们现在转移到“in head”模式,然后转移到“after head”。至此,body符号会被再处理,将创建一个HTMLBodyElement并插入到树中,同时状态会迁移到“in body”模式。
现在接受到字符串“Hello World”的字符符号,第一个字符将导致创建并插入一个文本节点,其他的字符将附加到该节点上。
接收到body结束符号时将转移到“after body”模式,接着我们接收到html结束符号时状态就转移到“after after body”模式。当接收到文件结束符时,整个解析过程就结束了。
2.4.2.8 解析结束时的处理
在这个阶段浏览器将文档标记为可交互的,并开始解析处于延时模式中的脚本——这些脚本在文档解析后执行。然后文档状态将被设置为完成,同时触发一个load事件。
符号化和构建树的完整算法,请参考HTML5 规范。
2.4.2.9 浏览器容错机制
你从来不会在一个HTML页面上看到“无效语法”这样的错误,因为浏览器修复了无效内容并继续工作。以下面这段HTML为例:
<html>
<mytag></mytag>
<div><p></div>Really lousy HTML</p>
</html>
这段HTML代码违反了很多规则(“mytag”不是合法的标签,“p”和“div”错误的嵌套等),但浏览器没有任何怨言地继续显示,它在解析过程中修复了HTML作者的错误。
浏览器具有一致的错误处理能力,但令人惊讶的是,这并不是当前HTML规范中的内容,就像书签和前进/后退按钮一样,它只是浏览器长期发展的结果。一些比较知名的非法HTML结构在许多站点出现过,浏览器都试着以一种和其他浏览器一致的方式去修复它们。
HTML5规范确实定义了这方面的需求,Webkit在HTML解析类开头的注视中,做了很好的总结。
解析器将符号化的输入解析为文档,构建文档树。如果文档是格式良好的,解析过程就很简单。但不幸的是,我们必须处理许多非格式良好的HTML文档,因此解析器必须能容忍错误。我们至少应该小心以下几种错误情况:
- 在未闭合的标签中,添加明令禁止的元素。在这种情况下应该先将前面的标签关闭,然后将禁止的标签添加到它的后面。
- 不能直接添加元素。有些人在写文档的时候会忘了一些中间标签(或者中间标签是可选的),比如:HTML HEAD BODY TR TD LI等。
- 想在行内元素中添加块状元素,必须先关闭所有的行内元素,直到下一个更高的块状元素。
- 如果这些都不行,就闭合当前标签直到我们允许添加该元素或忽略该标签。
下面来看一些Webkit容错的例子:
</br>
代替<br>
一些网站为了兼容IE和Firefox,使用</br>
代替<br>
,Webkit统一将它们视为<br>
。代码实现如下:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
注意:这里的错误处理在内部进行,用户看不到。
乱入的表格
乱入的表格是指一个表格嵌套在另一个表格中,并且还不是在它的某个单元格内。比如下面这个例子:
<table>
<table>
<tr><td>inner table</td></tr>
</table>
<tr><td>outer table</td></tr>
</table>
Webkit会把嵌套的表格变成两个兄弟表格:
<table>
<tr><td>outer table</td></tr>
</table>
<table>
<tr><td>inner table</td></tr>
</table>
代码实现如下:
if (m_inStrayTableContent && localName == tableTag)
popBlock(tableTag);
嵌套的表单元素
这种情况是指用户将一个表单嵌套到另一个表单中,则第二个表单会被忽略掉。代码实现如下:
if (!m_currentFormElement) {
m_currentFormElement = new HTMLFormElement(formTag, m_document);
}
太深的标签继承
代码注视说的很明白。
http://www.liceo.edu.mx 是一个嵌套层次过深的网站示例,它实现了约1500个标签的嵌套,全都来自一大堆的
<b>
标签。我们最多只允许20个相同类型的标签嵌套,多出来的将被忽略。
实现代码如下:
bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{
unsigned i = 0;
for(HTMLStackElem* curr = m_blockStack;
i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
curr = curr->next, i++) {}
return i != cMaxRedundantTagDepth;
}
放错地方的html或body结束标签
代码注视又一次解释地很清楚。
支持不完整的HTML。我们从来不闭合body标签,因为一些愚蠢的网页总是在还未真正结束的时候就闭合它。我们依赖调用
end()
方法来执行关闭的处理。
代码实现如下:
if (t->tagName == htmlTag || t->tagName == bodyTag)
return ;
所以Web开发者要小心了,除非你想成为Webkit容错代码的示例,否则还是写格式良好的HTML吧。
2.4.3 CSS 解析
还记得简介中提到的解析的概念吗?不像HTML,CSS属于上下文无关的文法,因此可以用前面描述的解析器来解析。事实上CSS规范定义了CSS的词法和语法文法。
看如下这个例子,每个符号都由正则表达式定义了词法(词汇表):
comment \/\*[^*]*\*+([^/*][^*]*\*+)*\/
num [0-9]+|[0-9]*"."[0-9]+
nonascii [\200-\377]
nmstart [_a-z]|{nonascii}|{escape}
nmchar [_a-z0-9-]|{nonascii}|{escape}
name {nmchar}+
ident {nmstart}{nmchar}*
“ident”是标识符的缩写,相当于一个类名;“name”是一个元素的ID(用“#”引用)。
语法用BNF描述:
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
selector
: simple_selector [ combinator selector | S+ [ combinator selector ] ]
;
simple_selector
: element_name [ HASH | class | attrib | pseudo ]*
| [ HASH | class | attrib | pseudo ]+
;
class
: '.' IDENT
;
element_name
: IDENT | '*'
;
attrib
: '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
[ IDENT | STRING ] S* ] ']'
;
pseudo
: ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
;
解释:一个ruleset是这样的结构:
div.error, a.error {
color: red;
font-weight: bold;
}
div.error
和a.error
是选择器,大括号中的内容包含了这条ruleset中的规则,这个结构在下面的定义中正式定义了:
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
这说明,一个ruleset具有一个或多个选择器,这些选择器用逗号和空格(S表示空格)进行分隔。每个ruleset包含花括号以及花括号中的一条或多条以分号隔开的声明。“declaration”和“selector”的定义在后面的BNF定义中。
2.4.3.1 Webkit CSS 解析器
Webkit使用Flex和Bison解析器生成器从CSS文法文件中自动生成解析器。回忆一下解析器的介绍,Bison创建了一个自底向上的递进解析器。Firefox自己写了一个自顶向下的解析器。两种解析器都会把每个CSS文件解析成样式表对象(StyleSheet Object),每个对象都包含CSS规则。CSS规则对象包含选择器和声明对象,以及其他与CSS语法对应的对象。
2.4.4 脚本解析
本章将介绍如何处理JavaScript。
2.4.5 处理脚本和样式表的顺序
2.4.5.1 脚本
Web模式是同步的,开发者希望当解析到一个script标签时,能立即解析执行脚本,文档的解析会被阻塞,直到脚本执行完。如果脚本是外引的,则必须通过网络请求到该资源——这个过程也是同步的,也会阻塞文档的解析直到资源被请求到。这个模式保持了很多年,并且在HTML4和HTML5规范中都特别指定了。开发者可以将脚本标记为“defer”,这样它就不会阻塞文档的解析,并且会在文档解析结束后执行。HTML5新增了标记脚本为异步的选项,这样会使用另一个线程解析执行脚本。
2.4.5.2 预解析
Webkit和Firefox都做了这个优化,当执行脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而提高整体的速度。值得注意的是,预解析并不会修改DOM树,它将这个工作留给主解析器,它只会解析外部资源引用,比如外部脚本、样式表以及图片。
2.4.5.3 样式表
样式表采用另一种不同的模式。理论上来说,既然样式表不会改变DOM树,就没必要停下文档的解析等待它们。然而存在一个问题,在文档的解析过程中脚本可能会请求样式信息,如果样式还没有加载和解析,脚本将得到错误的结果,很明显这将会导致很多问题。这看起来是个边缘情况,但确实很常见。Firefox在样式表加载和解析的时候会阻塞所有的脚本,而Chrome只有在当脚本访问某些未加载的样式表所影响的特定的样式属性时,才阻塞这些脚本。
2.5 渲染树的构造
当DOM树构建完后,浏览器开始构建另一棵树——渲染树。渲染树是由元素显示序列中的可见元素组成,它是文档的可视化表示,构建这棵树是为了以正确的顺序绘制文档的内容。
Firefox将渲染树中的元素称为“frames”,Webkit则使用术语“renderer”或“render object”。一个renderer知道怎么布局以及绘制自己和它的子元素。
RenderObject是Webkit渲染对象的基类,它的定义如下:
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盒模型相对应,在CSS2规范中定义了该盒模型,它包含诸如宽、高和位置之类的几何信息。
盒模型的类型受对应节点的display
样式属性的影响(参考样式计算章节)。下面的Webkit代码说明了如何根据display
属性决定为某个节点创建何种类型的渲染对象。
RenderObject* RenderObject::createObject(Node* node, RenderStyle* style)
{
Document* doc = node->renderArena();
RenderArena* arena = doc->renderArena();
...
RenderObject* o = 0;
switch(style->display()) {
case NODE:
break;
case INLINE:
o = new (arena) RenderInline(node);
break;
case BLOCK:
o = new (arena) RenderBlock(node);
break;
case INLINE_BLOCK:
o = new (arena) RenderInlineBlock(node);
break;
case LIST_ITEM:
o = new (arena) RenderListItem(node);
break;
...
}
return o;
}
当然,元素的类型也需要考虑,例如表单控件和表格带有特殊的框架。在Webkit中,如果一个元素想要创建特殊的渲染对象,它需要重写createRenderer
方法,使渲染对象指向不包含几何信息的样式对象。
2.5.1 渲染树和DOM树的关系
渲染对象和DOM元素相对应,但这种关系不是一一对应的。不可见的DOM元素不会被插入到渲染树,例如“head”元素。此外,display
属性为none
的元素也不会出现在渲染树中(visibility
属性为hidden
的元素会出现在渲染树中)。
有一些DOM元素对应几个可见对象,它们一般是具有复杂结构的元素,无法用一个矩形来描述。例如,“select”元素有3个渲染对象——一个显示区域、一个下拉列表和一个按钮。同样,当文本因为宽度不够而换行时,新行将作为额外的元素被添加。另一个多渲染对象的例子是不规范的HTML。根据CSS规范,一个行内元素只能仅包含行内元素或仅包含块状元素,在存在混合内容时,将会创建匿名的块状渲染对象包裹住行内元素。
一些渲染对象和所对应的DOM节点不在树上相同的位置。例如,浮动和绝对定位的元素在正常流之外,在两棵树上的位置不同,渲染树上标志出真实的结构,并用一个占位结构标志出它们原来的位置。
2.5.2 创建树的流程
Firefox中,表现为注册DOM更新的监听器,将frame的创建委派给“FrameConstructor”,这个构建器解析样式(参考样式计算)并创建一个frame。
在Webkit中,解析样式并生成渲染对象的过程称为“attachment”,每个DOM节点都有一个“attach”方法,attachment的过程是同步的,调用新节点的attach方法将节点插入到DOM树中。
处理html和body标签将构建渲染树的根,这个根渲染对象对应CSS规范中的“containing block”——包含其他所有blocks的顶级block。它的大小就是viewport——浏览器窗口的显示区域。Firefox称它为“ViewPortFrame”,而Webkit称它为“RenderView”。这个就是文档所指的渲染对象,树中其他部分将作为插入的DOM节点被创建。详细内容,请参阅CSS2的相关主题——Processing Model。
2.5.3 样式计算
构建渲染树需要计算出每个渲染对象的可视属性,这可以通过计算每个元素的样式属性得到。
样式包括各种来源的样式表,行内样式元素以及html中的可视化属性(如“bgcolor”),之后会将它转化为CSS样式属性。
样式表来源于浏览器的默认样式表,页面作者提供的样式表和用户提供的样式表——这些样式表是浏览器用户提供的(浏览器允许用户自定义喜欢的样式。例如,在Firefox中,可以通过在“Firefox Profile”目录下放置样式表来实现)。
样式计算有一些困难:
- 样式数据是一个非常大的结构,保存大量的样式属性会导致内存问题。
- 如果不进行优化,找到每个元素匹配的规则会导致性能问题,为每个元素查找匹配的规则都需要遍历整个规则表,这个工作量非常大。选择器可能有复杂的结构,匹配过程如果沿着一条开始看似正确,后来被证明是无用的路径,则必须去尝试另一条路径。例如下面这个复杂的选择器:
div div div div {
...
}
这意味着需要把规则应用到三个div的后代div元素上,假设你想要检查该规则是否已应该应用到某个给定的“<div>”元素上,你选择树上一条特定的路径去检查,这可能需要遍历节点树,最后却发现它只是两个div的后台,并不能应用该规则。然后你不得不尝试另外一条路径。
- 应用规则涉及非常复杂的级联规则,它们定义了规则的层次。
让我们来看一下浏览器是如何处理这些问题的:
2.5.3.1 共享样式数据
Webkit节点引用的样式对象(RenderStyle)在某些情况下可以被节点间共享,这些节点必须是兄弟或者表兄弟节点,并且满足以下条件:
- 这些元素必须处于相同的鼠标状态(比如:不能一个处于hover,另一个不是)
- 元素不能具有ID
- 标签名必须匹配
- class属性必须匹配
- 映射的属性集必须是相同的
- 链接的状态必须匹配
- 焦点的状态必须匹配
- 不能有元素被属性选择器影响
- 元素不能有行内样式属性
- 不能使用兄弟选择器,WebCore在遇到兄弟选择器时,只是简单地抛出一个全局转换,并且在它们显示时使整个文档的样式共享失效,这些包括
+
选择器和类似:first-child
和:last-child
这样的选择器
2.5.3.2 Firefox 规则树
Firefox用两棵树来简化样式计算——规则树和样式上下文树。Webkit也有样式对象,但它们并没有存储在类似上下文树这样的树中,只是由DOM节点指向其关联的样式。
样式上下文包含最终值,这些值是通过以正确的顺序应用所有匹配的规则,并将它们由逻辑值转换为具体的值。例如,如果逻辑值是屏幕百分比,则通过计算将其转换为绝对单位。使用规则树这个注意确实很巧妙,它允许节点中共享这些只,而不需要重复计算,同时也节省了存储空间。
所有匹配的规则都存储在规则树中,一条路径中的最底层节点拥有最高的优先级,这棵树包含了已经找到的所有匹配规则的路径。存储规则是懒加载的,规则树并不是一开始就为每个节点进行计算,而是在某个节点需要计算样式的时候才进行相应的计算,并将计算后的路径添加到树中。
我们将树上的路径看成词典中的单词,假如已经计算出了如下的规则树:
假如要为内容树中的另一个节点匹配规则,现在知道匹配的规则(以正确的顺序)是”B-E-I”,因为我们已经计算出了路径“A-B-E-I-L”,所以树上已经存在这条路径,现在剩下的工作就很少了。
现在来看下树是如何保存工作的。
2.5.3.2.1 结构化
样式上下文按结构进行划分,这些结构包含类似border
或color
这样的特定分类的样式信息。结构中的所有属性不是继承的就是非继承的,对继承的属性,除非元素自身有定义,否则就从它的parent那继承。非继承的属性(又称“reset”属性)如果没有定义,则使用默认值。
样式上下文树通过缓存完整的结构(包含计算后的值)来帮助我们,这样如果底层节点没有为一个结构提供定义,则使用上层节点缓存的结构。
2.5.3.2.2 使用规则树计算样式上下文
当为一个特定的元素计算样式时,首先计算出规则树中的一条路径,或者使用已经存在的一条,然后用路径中的规则去填充新的样式上下文结构。从路径的底层节点开始,它具有最高的优先级(通常是最特定的选择器),遍历规则树,直到填满我们的结构。如果在那个规则节点没有定义所需的结构规则,我们就可以大大地进行优化——我们可以沿着树向上查询,直到找到一个指向该规则的节点——这是最好的优化,整个结构都被共享了,这也节省了最终值的计算和内存。
如果我们找到部分定义,我们会继续沿着树往上查询,直到结构体被填满。如果最终没有找到该结构的任何规则定义,那么如果这个结构是继承型的,我们就指向上下文树中的parent结构,在这种情况下,我们也成功的共享了结构;如果这个结构是reset型的,则使用默认的值。
如果最特定的节点添加了值,那么我们需要做一些额外的计算将其转换为实际值,然后将结果缓存在树上的节点,这样它就可以被子节点所用。
当一个元素和它的兄弟元素指向同一个树节点时,整个样式上下文 都可以被它们共享。
来看一个例子,假如有下面这段HTML:
<html>
<body>
<div class="err" id="div1">
<p>
this is a <span class="big"> big error </span>
this is also a
<span class="big"> very big error </span> error
</p>
</div>
<div class="err" id="div2"></div>
</body>
</html>
以及下面这些规则:
1. div { margin: 5px; color: black; }
2. .err { color: red; }
3. .big { margin-top: 3px; }
4. div span { margin-bottom: 4px; }
5. #div1 { color: blue; }
6. #div2 { color: green; }
简化下问题,我们只填充两个结构——color和margin,color结构只包含一个成员——颜色,margin结构包含四边。
生成的规则树如下(节点名:指向的规则):
上下文树如下(节点名:指向的规则节点):
假如我们解析HTML碰到第二个<div>
标签,我们需要为这个节点创建样式上下文,并填充它的样式结构。我们要进行规则匹配,发现这个<div>
匹配的规则为1、2、6,我们发现规则树上已经存在一条我们可以使用的路径1、2,我们只需为规则6新增一个节点添加到下面(就是规则树中的F)。我们会创建一个样式上下文并将其放到上下文树中,新的样式上下文将指向规则树中的节点F。
我们现在需要填充这个样式的上下文,先从填充margin结构开始,既然最后一个规则节点F没有添加margin结构,沿着路径向上,直到找到缓存的前面插入节点计算出的结构,我们发现节点B是最近的指定margin值的节点。
因为已经有了color结构的定义,所以不能使用缓存的结构。既然color只有一个属性,所以也就不需要沿着路径向上填充其他属性。我们会计算出最终值(将字符串转换为RGB等),并将计算后的结构缓存在节点上。
第二个<span>
元素更简单,进行规则匹配后发现它指向规则G,和前一个<span>
一样,和前一个<span>
一样。既然有兄弟节点指向同一个节点,就可以共享整个样式上下文,只需指向前一个<span>
的上下文。
因为结构中包含继承自parent的规则,上下文树做了缓存(color属性是继承来的,但Firefox将其视为reset并在规则树中缓存)。例如,如果我们为一个段落添加如下规则:
p { font-family: Verdana; font-size: 10px; font-weight: bold; }
那么这个<p>
在内容树中的子节点<div>
,会共享和它parent一样的font结构,这种情况发生在没有为这个<div>
指定font规则时。
在Webkit中并没有规则树,匹配声明会被遍历四次。首先应用非important
的高优先级属性(之所以先应用这些属性,是因为其他依赖于它们,比如:display
属性),其次是高优先级important
,接着是一般优先级的非important
,最后是一般优先级的important
的规则。这意味着出现多次的属性将被按照正确的级联顺序进行处理,最后一个生效。
总结一下,共享样式对象(整个结构或结构的部分属性)解决了问题1和3。Firefox的规则树也对以正确顺序应用规则起到帮助。
2.5.3.3 处理规则以简化匹配
样式规则有几个来源:
- 来自外部样式表或
<style>
标签中的CSS规则,如:p { color: blue; }
- 行内样式属性,如:
<p style="color: blue"></p>
- HTML可视化属性(映射为对应的样式规则),如:
<p bgcolor="blue"></p>
后面两个很容易匹配到元素,因为它们所拥有的样式属性和HTML属性可以将元素作为key进行映射。
就像前面问题2提到的,CSS的规则匹配很复杂,为了解决这个问题,可以先对规则进行处理,使其更容易访问。
解析完样式表之后,规则会根据选择符被添加到一些哈希表。这些表可以是根据id、class、标签名或任何不属于这三个分类的通用映射表。如果选择符是id,规则将被添加了id映射表中;如果是class,则被添加到class映射表中,等等。这个处理简化了匹配规则,没必要查看每个声明,我们可以从映射表中找到一个元素的相关规则。这个优化覆盖了 95+% 的规则,在匹配过程中就可以不考虑这些规则了。
例如,看下面的样式规则:
p.error { color: red; }
#messageDiv { height: 50px; }
div { margin: 5px; }
第一条规则将被插入class映射表,第二条规则插入id映射表,第三条长路标签映射表。对于下面这段HTML:
<p class="error">an error occurred</p>
<div id="messageDiv">this is a message</div>
我们首先找到p元素对应的规则,class映射表包含一个“error”的key,根据key可以找到p.error
的规则。div元素在id映射表(key就是对应的id)和标签映射表中都有相关的规则,剩下的工作就是找出这些key对应的规则中,哪些是真正匹配的。例如,如果div的规则是:
table div { margin: 5px; }
这个规则也是从标签映射表中获得的,因为key是最右边的选择符,但它并不匹配这里的div元素,因为HTML片段中的div并没有table祖先。
Webkit和Firefox都会做这个处理。
2.5.3.4 以正确的级联顺序应用规则
样式对象的属性对应所有可见属性(所有CSS属性,但是更通用)。如果属性没有被任何匹配的规则所定义,那么一些属性可以从parent的样式对象中继承,另一些使用默认值。
问题产生于存在不止一处的定义,我们可以用级联顺序来解决这个问题。
2.5.3.4.1 样式表的级联顺序
一个样式属性的声明可能出现在几个样式表中,或者在一个样式表中出现多次。这意味着应用规则的顺序至关重要,这个顺序就是级联顺序。根据CSS2的规范,级联顺序为(从低到高):
- 浏览器的声明
- 用户的一般声明
- 作者的一般声明
- 作者的important声明
- 用户的important声明
浏览器的声明是最不重要的,用户的声明只有被标记为important的时候才会覆盖作者的声明。具有同等级别的声明将根据 特殊性(specifity) 以及它们被定义时的顺序进行排序。HTML的可视化属性会被转换成匹配的CSS声明,它们被视为最低优先级的作者规则。
2.5.3.4.2 选择符的特殊性
CSS2规范 中定义的选择器特殊性如下:
- 如果声明来自style属性,而不是一个选择器的规则,则计为1,否则计为0(=a)
- 计算选择器中id属性的数量(=b)
- 计算选择器中class以及伪类的数量(=c)
- 计算选择器中元素名以及伪元素的数量(=d)
连接a-b-c-d
四个数字(用大基数的计算系统)将得到选择器的特殊性(specifity)。例如,如果a为14,你可以使用16进制;如果a为17,则需要使用十七进制,这种情况可能发生在选择符为html body div div p ...
(选择符中有17个标签,一般不太可能)。
这里有一些特殊性计算的例子:
* {} /* a=0 b=0 c=0 d=0 -> specificity = 0,0,0,0 */
li {} /* a=0 b=0 c=0 d=1 -> specificity = 0,0,0,1 */
li:first-line {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul li {} /* a=0 b=0 c=0 d=2 -> specificity = 0,0,0,2 */
ul ol+li {} /* a=0 b=0 c=0 d=3 -> specificity = 0,0,0,3 */
h1 + *[rel=up]{} /* a=0 b=0 c=1 d=1 -> specificity = 0,0,1,1 */
ul ol li.red {} /* a=0 b=0 c=1 d=3 -> specificity = 0,0,1,3 */
li.red.level {} /* a=0 b=0 c=2 d=1 -> specificity = 0,0,2,1 */
#x34y {} /* a=0 b=1 c=0 d=0 -> specificity = 0,1,0,0 */
style="" /* a=1 b=0 c=0 d=0 -> specificity = 1,0,0,0 */
2.5.3.4.3 规则排序
规则匹配后,需要根据级联顺序对规则进行排序。Webkit中对小列表使用冒泡排序,大列表用归并排序。Webkit通过为规则重载“>”操作符来执行排序:
static bool operator >(CSSRuleData& r1, CSSRuleData& r2)
{
int spec1 = r1.selector()->specificity();
int spec2 = r2.selector()->specificity();
return (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2;
}
2.5.4 逐步处理
Webkit使用一个标志位标识所有顶层样式表(包括@imports
)是否已经加载,如果在attaching的时候样式表没有完全加载,则放置占位符,并在文档中标记,一旦样式表完成加载就重新进行计算。
2.6 布局(Layout)
当渲染对象被创建并添加到树中,它们并没有位置和大小,计算这些值的过程称为layout或reflow。
HTML使用基于流的布局模型,这意味着大多数时候可以以单一的途径进行几何计算。流中靠后的元素并不会影响前面元素的几何特性,所以布局可以在文档中从左到右、自上而下的进行。也存在一些例外,比如HTML的table可能需要不止一行。
坐标系统相对于根frame,使用top和left坐标。
布局是个递归的过程,由根渲染对象开始,它对应HTML文档元素。布局继续递归的通过一些或所有的frame层级,为每个需要几何信息的渲染对象进行计算。
根渲染对象的位置是0,0
,它的大小是viewport——浏览器窗口的可见部分。
所有的渲染对象都有一个layout或reflow方法,每个渲染对象调用需要布局的children的layout方法。
2.6.1 脏点系统(Dirty bit system)
为了不因每个小变化都全部重新布局,浏览器使用了“dirty bit”系统。如果一个渲染对象发生了变化或者被添加了,就标记它以及它的children都为“dirty”——需要重新布局。这里有两种标志——“dirty”和“children are dirty”,“Children are dirty”说明即使这个渲染对象可能没问题,但它至少有一个child需要重新布局。
2.6.2 全局布局和增量布局
布局在整棵渲染树上触发时,称为全局布局,下面两种情况可能发生全局布局:
- 一个全局的样式改变影响所有的渲染对象,比如:
font-size
的改变。 - 窗口resize
布局也可以是增量进行的,这样只有标记为“dirty”的渲染对象会重新布局(也会导致一些额外的布局)。增量布局会在渲染对象为“dirty”的时候(异步)触发。例如,当网络接收到新的内容并添加到DOM树后,新的渲染对象会添加到渲染树中。
2.6.3 异步布局和同步布局
增量布局是异步完成的。Firefox为增量布局生成了“reflow”队列以及一个调度器触发这些批处理命令。Webkit也有一个计时器用来执行增量布局——遍历树并为“dirty”状态的渲染对象重新布局。此外,当脚本请求样式信息时,例如:offsightHeight
,会同步地触发增量布局。全局布局一般都是同步触发的。有的时候布局会被作为一个初始布局的回调,因为一些属性发生了改变,比如滚动条的位置发生改变。
2.6.4 优化
当一个布局因为resize或渲染位置(不是大小)的改变而触发时,渲染对象的大小将会从缓存中读取,而不会重新计算。某些情况下,如果只有子树被修改了,则布局并不从根开始。这种情况可能发生,比如变化发生在元素自身并且不影响它周围元素,例如将文本插入文本框中(否则每次击键都将触发从根开始的重排)。
2.6.5 布局过程
布局通常有以下几种模式:
- Parent渲染对象决定它的宽度。
- Parent渲染对象读取children,并且:
- 放置child渲染对象(设置它的x和y)。
- 在需要时(它们当前为“dirty”或处于全局布局状态下或是其他原因)调用child渲染对象的layout,这将计算child的高度。
- Parent渲染对象使用child渲染对象的累积高度以及margin和padding的高度来设置自己的高度——这将被parent渲染对象的parent使用。
- 将它的“dirty”标志设置为
false
Firefox使用一个“state”对象(nsHTMLReflowState)作为参数去布局(在Firefox中称为reflow),state对象包含parent的宽度及其他内容。Firefox布局的输出是一个“metrics”对象(nsHTMLReflowMetrics),它包括渲染对象的高度。
2.6.6 宽度计算
渲染对象的宽度使用容器的宽度、渲染对象样式中的宽度以及margin、border进行计算。例如,下面这个div的宽度:
<div style="width:30%"></div>
Webkit中宽度的计算过程如下(RenderBox
类的calcWidth
方法):
- 容器的宽度是容器可用宽度和0中的最大值,这里的可用宽度是内容宽度,它等于:
contentWidth = clientWidth() - paddingLeft() - paddingRight()
,clientWidth和clientHeight代表一个对象内部不包括border和滑动条的大小。 - 元素的宽度是指样式属性
width
的值,它可以通过计算父容器宽度的百分比得到一个绝对值。 - 加上水平方向上的border和padding
到此位置,这就是“最佳宽度”的计算过程,现在计算宽度的最大值和最小值。如果最佳宽度大于最大宽度,则使用最大宽度;如果最佳宽度小于最小宽度,则使用最小宽度。最后缓存这个值,当需要重新布局并且宽度未改变的时候会被重复用到。
2.6.7 换行
当一个渲染对象在布局过程中需要换行时,它会暂停并告诉它的parent它需要换行,parent会创建额外的渲染对象并调用它们的layout方法。
2.7 绘制(Painting)
在绘制阶段,会遍历渲染树并调用渲染对象的“paint”方法,将它们的内容显示在屏幕上,绘制使用UI基础组件,这在UI的章节会有更多的介绍。
2.7.1 全局和增量
和布局一样,绘制也可以是全局的(绘制完整的树)或增量的。在增量绘制过程中,一些渲染对象以不影响整棵树的方式改变,发生改变的渲染对象使其在屏幕上的矩形区域失效,这会导致操作系统将其看作“dirty region”,并产生一个“paint”事件,操作系统很巧妙地将多个区域合并为一个。在Chrome中这个过程更复杂点,因为渲染对象在不同的进程中,而不是在主进程中。Chrome在一定程度上模拟了操作系统的行为,表现为监听事件并派发消息给渲染根,遍历树直到找到相关的渲染对象,重绘这个对象(通常还会重绘它的children)。
2.7.2 绘制顺序
CSS2定义了绘制过程的顺序,这实际上是元素压入堆栈上下文的顺序,这个顺序影响着绘制,因为堆栈是从后向前绘制。一个块渲染对象的堆栈顺序是:
- background color
- background image
- border
- children
- outline
2.7.3 Firefox 显示列表
Firefox读取渲染树并为绘制的矩形创建一个显示列表,该列表以正确的绘制顺序包含这个矩形相关的渲染对象(渲染对象的背景、边框等)。用这种方法可以使重绘只需查找一次树,而不需要多吃查找——绘制所有的背景、所有的图片、所有的边框等。Firefox优化了这个过程,它不会添加被隐藏的元素,比如元素完全在其他不透明元素下面。
2.7.4 Webkit 矩形存储
重绘前,Webkit会将旧的矩形保存为位图,然后只重绘新旧矩形的差集。
2.8 动态变化
浏览器总是以尽可能小的动作响应一个变化,所以一个元素颜色的变化只会导致该元素的重绘,元素位置的变化将导致该元素、它的子元素和兄弟元素的重新布局和重绘。添加一个DOM节点,也会导致这个元素的布局和重绘。一些主要的变化,比如增加“html”元素的font-size
,将会导致缓存失效,从而引起整棵树的重新布局和重绘。
2.9 渲染引擎的线程
渲染引擎是单线程的,除了网络操作外,几乎所有的事情都在这个单一的线程中处理。在Firefox和Safari中,这是浏览器的主线程;在Chrome中,这是它tab的主线程。
网络操作是由几个并行的线程执行,并行连接的个数是受限的(通常是2-6个连接)。
2.9.1 事件循环
浏览器的主线程是一个事件循环,它被设计为无限循环,以保持执行过程的可用,它一直等待事件(例如layout和paint事件)并执行它们。下面是Firefox的主要事件循环代码:
while (!mExiting)
NS_ProcessNextEvent(thread);
2.10 CSS2 可视化模型
2.10.1 画布(The canvas)
根据CSS2规范,术语canvas是用来描述“格式化结构所渲染的空间”——浏览器绘制内容的地方。Canvas对每个维度空间都是无限大的,但是浏览器基于viewport的大小选择了一个初始宽度。根据zindex.html的定义,canvas如果是位于其他canvas内则是透明的,否则浏览器会指定一个颜色。
2.10.2 CSS 盒模型
CSS 盒模型描述了矩形盒,这些矩形盒是为了文档树中的元素生成的,并根据可视的格式化模型进行布局。每个盒子包括内容区域(如图片、文本等)及可选的四周padding、border和margin区域。
每个节点生成0到n个这样的box,所有的元素都有一个display
属性,用来决定它们生成box的类型,例如:
block - 生成块状block
inline - 生成一个或多个行内block
none - 不生成block
默认是inline
,但是浏览器样式设置了其他默认值。例如,div元素默认是display: block;
,你可以访问这里查看更多的默认样式表例子。
2.10.3 定位策略(Positioning scheme)
这里有三种定位策略:
- normal - 对象根据它在文档中的位置来定位,这意味着它在渲染树和DOM树中的位置是一致的,并根据它的盒模型和大小进行布局
- float - 对象先像正常的流一样布局,然后尽可能的向左或向右移动
- absolute - 对象在渲染树中的位置和DOM树中的位置无关
定位策略是通过设置position
属性和float
属性来实现的:
-
static
和relative
会导致normal
定位 -
absolute
和fixed
会导致absolute
定位
在static
定位中,不定义位置而使用默认的位置。在其他策略中,作者指定位置——top
, bottom
, left
, right
。
Box布局的方式由这几项决定:
- Box类型
- Box大小
- 定位策略
- 扩展信息(比如图片的大小和屏幕尺寸)
2.10.4 Box 类型
Block Box:构成一个块,在浏览器窗口上有自己的矩形。
Inline Box:并没有自己的块状区域,包含在一个块状区域内。
block是一个挨着一个垂直格式化,inline则在水平方向上格式化。
Inline boxes放置在行内 box中,每行至少和最高的box一样高,当box以baseline
对齐时,即一个元素的底部和另一个box上除底部以外的某点对齐,行高可以比最高的box高。当容器宽度不够时,行内元素将被放到多放中,这在p元素中经常发生。
2.10.5 定位(Positioning)
2.10.5.1 Relative
相对定位,先按照一般的定位,然后按所要求的差值移动。
2.10.5.2 Floats
一个浮动的box移动到一行的最左边或最右边,其余的box围绕在它周围。下面这段HTML:
<p>
<x:img style="float: right;" src="images/image.gif" width="100" height="100">
Lorem ipsum dolor sit amet, consectetuer...
</p>
将显示为:
2.10.5.3 Absolute and fixed
这种情况下的布局完全不顾普通的文档流,元素不属于文档流的一部分,大小取决于容器。在Fixed布局中,容器就是viewport(可视区域)。
注意:fixed元素即使文档滚动时也不会移动。
2.10.6 分层表示(Layered representation)
这是有CSS属性中的z-index
指定,表示盒模型的第三个大小,即在z轴上的位置。Box分发到堆栈中(称为堆栈上下文),每个堆栈中靠后的元素将较早的绘制,栈顶靠前的元素离用户最近,当发生交叠时,将隐藏靠后的元素。堆栈根据z-index
属性排列,拥有z-index
属性的box形成了一个局部堆栈,viewport有外部堆栈,例如:
<style type="text/css">
div {
position: absolute;
left: 2in;
top: 2in;
}
</style>
<p>
<div style="z-index:3;background-color:red;width:1in;height:1in;"></div>
<div style="z-index:1;background-color:green;width:2in;height:2in;"></div>
</p>
虽然绿色div排在红色div后面,可能在正常流中也已经被绘制在后面,但z-index
有更高的优先级,所以在根box的堆栈中更靠前。