[译]浏览器如何工作

常见的浏览器

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 浏览器的主要构成

浏览器的主要组件包括:

  1. 用户界面——包括地址栏、后退/前进按钮、书签菜单等,也就是你看到的除了用来显式你请求页面的主窗口之外的其余部分。
  2. 浏览器引擎——查询和操作渲染引擎的接口。
  3. 渲染引擎——用来显示请求的内容。例如:如果请求内容为HTML,它负责解析HTML和CSS,并将解析后的结果在屏幕上显示。
  4. 网络——用来完成网络调用,比如HTTP请求。它具有平台无关的接口,在不同平台实现不同。
  5. UI后端——用来绘制基本组件,例如:组合下拉框和窗口等。具有平台无关的通用接口,底层使用操作系统的用户接口实现。
  6. JavaScript解释器——用来解释和执行JavaScript代码。
  7. 数据存储——属于持久层。浏览器需要在硬盘上保存各种各样的数据,例如Cookies。HTML5规范中定义了Web Database技术,这是一种完整(且轻量)的浏览器端数据库。
图1:浏览器的主要组件

值得注意的是,不同于大多数的浏览器,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树

图2:渲染引擎基本流程

渲染引擎开始解析HTML文档,并将标签转为内容树中的DOM节点。接下来,它解析外部CSS文件和style标签中的样式信息。这些样式信息和HTML中的可见指令将被用来构建另一棵树——Render树(渲染树)。

Render树由一些包含视觉属性(如颜色和大小)的矩形组成,它们将按照正确的顺序显示到屏幕上。

Render树构建好之后将会执行布局过程,这意味着它将确定每个节点在屏幕上的确切坐标。下一步就是绘制——遍历Render树并使用UI后端层绘制每个节点。

值得注意的是,这个过程是逐步完成的。为了更好的用户体验,渲染引擎会尽可能早的将内容呈现到屏幕上,并不会等到所有的HTML都解析完成之后再去构建和布局Render树。它是解析完一部分内容就显示一部分内容,同时从网络上下载剩余内容。

值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时进程还在从网络上下载其余内容。

2.3 主流程案例

图3:Webkit主流程
图4:Mozilla的Gecko渲染引擎主流程

从图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这个表达式可能返回这样一棵树:

图5:数学表达式树节点

2.4.1.1 文法

解析基于文档遵循的语法规则——写入文档的语言或格式。每种能被解析的格式,必须具有词汇以及语法规则组成的特定的文法,称为上下文无关文法。人类语言不具有这种特性,因此不能被传统的解析技术所解析。

2.4.1.2 解析器与词法分析器

解析可以分为两个子过程——词法分析和语法分析。

词法分析就是将输入分解为符号,符号就是语言的词汇表——有效构建块的集合。在人类语言中,相当于这门语言字典中出现的所有单词。

语法分析是指对语言应用语法规则。

解析器通常将工作分配给两个组件——词法分析器(有时也叫分词器)负责将输入分解为合法的符号,解析器则根据语言的语法规则分析文档结构,从而构建解析树。词法分析器知道如何去掉无关字符(如空格和换行符)。

图6:从源文档到解析树

解析过程是迭代的。解析器总是会从词法分析器那取一个新的符号,并试着用这个符号匹配一条语法规则。如果匹配了一条规则,这个符号对应的节点将被添加到解析树上,然后解析器会继续请求下一个符号。如果没有匹配到规则,解析器会在内部保存该符号,并从词法分析器取下一个符号,直到所有内部保存的符号能够匹配一条语法规则。如果最终没有找到匹配的规则,解析器将抛出一个异常,这意味着文档是无效的或者包含语法错误。

2.4.1.3 转换

很多时候解析树并不是最终产品。解析一般在转换中使用——将输入文档转换成另一种格式。编译就是个例子,编译器将源代码编译成机器码的时候,先将源代码解析为解析树,然后将该树转换成机器码文档。

图7:编译流程

2.4.1.4 解析实例

在图5中,我们从一个数学表达式构建了一棵解析树。我们在这里定义一个简单的数学语言来分析下解析过程。

词汇表:我们的语言包括整数、加号以及减号。

语法:

  1. 该语言的语法基本单元包括表达式、terms以及操作符。
  2. 该语言可以包括多个表达式。
  3. 一个表达式定义为两个term通过一个操作符连接。
  4. 操作符可以是加号或减号。
  5. 一个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树:

图8:示例标签对应的DOM树

和HTML一样,DOM规范也是由W3C组织制定的,这是操作文档的一般规范,一个特定的模块描述一种特定的HTML元素。HTML的定义请查阅这里:idl-definitions.html

当我说树包含了DOM节点,我的意思是说该树是由实现了DOM接口的元素构建而成的,浏览器使用已被浏览器内部使用的其他属性的具体实现。

2.4.2.5 解析算法

正如前面章节中所讨论的,HTML不能被一般的自顶向下或自底向上的解析器所解析。原因如下:

  1. 语言本身的宽容性。
  2. 浏览器对一些常见的无效HTML有容错支持。
  3. 解析过程是往返的。通常情况下,源码不会在解析过程中发生改变,但在HTML中,脚本标签包含的document.write可能添加额外标签,所以在解析过程中实际上修改了输入。

不能使用正则解析技术,浏览器为了解析HTML,创建了专属的解析器。

HTML5规范中详细描述了这个解析算法,它由两个阶段组成——符号化和构建树。符号化阶段进行词法分析,将输入解析为符号。HTML符号包括开始标签、结束标签、属性名以及属性值。符号识别器识别出符号后,会将它传给树构建器,并读取下一个字符以识别下一个符号,循环此过程,直到处理完所有的输入。

图9:HTML解析流程,图片来源HTML5规范

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>”标签的处理跟前面一样。

图10:符号化示例输入

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”模式。当接收到文件结束符时,整个解析过程就结束了。

图11:示例HTML树构建过程

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文档,因此解析器必须能容忍错误。我们至少应该小心以下几种错误情况:

  1. 在未闭合的标签中,添加明令禁止的元素。在这种情况下应该先将前面的标签关闭,然后将禁止的标签添加到它的后面。
  2. 不能直接添加元素。有些人在写文档的时候会忘了一些中间标签(或者中间标签是可选的),比如:HTML HEAD BODY TR TD LI等。
  3. 想在行内元素中添加块状元素,必须先关闭所有的行内元素,直到下一个更高的块状元素。
  4. 如果这些都不行,就闭合当前标签直到我们允许添加该元素或忽略该标签。

下面来看一些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.errora.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语法对应的对象。

图12:解析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节点不在树上相同的位置。例如,浮动和绝对定位的元素在正常流之外,在两棵树上的位置不同,渲染树上标志出真实的结构,并用一个占位结构标志出它们原来的位置。

图13:渲染树及对应的DOM树,“Viewport”是最初的包含块,在Webkit中指“RenderView”对象

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”目录下放置样式表来实现)。

样式计算有一些困难:

  1. 样式数据是一个非常大的结构,保存大量的样式属性会导致内存问题。
  2. 如果不进行优化,找到每个元素匹配的规则会导致性能问题,为每个元素查找匹配的规则都需要遍历整个规则表,这个工作量非常大。选择器可能有复杂的结构,匹配过程如果沿着一条开始看似正确,后来被证明是无用的路径,则必须去尝试另一条路径。例如下面这个复杂的选择器:
div div div div {
  ...
}

这意味着需要把规则应用到三个div的后代div元素上,假设你想要检查该规则是否已应该应用到某个给定的“<div>”元素上,你选择树上一条特定的路径去检查,这可能需要遍历节点树,最后却发现它只是两个div的后台,并不能应用该规则。然后你不得不尝试另外一条路径。

  1. 应用规则涉及非常复杂的级联规则,它们定义了规则的层次。

让我们来看一下浏览器是如何处理这些问题的:

2.5.3.1 共享样式数据

Webkit节点引用的样式对象(RenderStyle)在某些情况下可以被节点间共享,这些节点必须是兄弟或者表兄弟节点,并且满足以下条件:

  1. 这些元素必须处于相同的鼠标状态(比如:不能一个处于hover,另一个不是)
  2. 元素不能具有ID
  3. 标签名必须匹配
  4. class属性必须匹配
  5. 映射的属性集必须是相同的
  6. 链接的状态必须匹配
  7. 焦点的状态必须匹配
  8. 不能有元素被属性选择器影响
  9. 元素不能有行内样式属性
  10. 不能使用兄弟选择器,WebCore在遇到兄弟选择器时,只是简单地抛出一个全局转换,并且在它们显示时使整个文档的样式共享失效,这些包括+选择器和类似:first-child:last-child这样的选择器

2.5.3.2 Firefox 规则树

Firefox用两棵树来简化样式计算——规则树和样式上下文树。Webkit也有样式对象,但它们并没有存储在类似上下文树这样的树中,只是由DOM节点指向其关联的样式。

图14:Firefox样式上下文树

样式上下文包含最终值,这些值是通过以正确的顺序应用所有匹配的规则,并将它们由逻辑值转换为具体的值。例如,如果逻辑值是屏幕百分比,则通过计算将其转换为绝对单位。使用规则树这个注意确实很巧妙,它允许节点中共享这些只,而不需要重复计算,同时也节省了存储空间。

所有匹配的规则都存储在规则树中,一条路径中的最底层节点拥有最高的优先级,这棵树包含了已经找到的所有匹配规则的路径。存储规则是懒加载的,规则树并不是一开始就为每个节点进行计算,而是在某个节点需要计算样式的时候才进行相应的计算,并将计算后的路径添加到树中。

我们将树上的路径看成词典中的单词,假如已经计算出了如下的规则树:

图15

假如要为内容树中的另一个节点匹配规则,现在知道匹配的规则(以正确的顺序)是”B-E-I”,因为我们已经计算出了路径“A-B-E-I-L”,所以树上已经存在这条路径,现在剩下的工作就很少了。

现在来看下树是如何保存工作的。

2.5.3.2.1 结构化

样式上下文按结构进行划分,这些结构包含类似bordercolor这样的特定分类的样式信息。结构中的所有属性不是继承的就是非继承的,对继承的属性,除非元素自身有定义,否则就从它的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结构包含四边。

生成的规则树如下(节点名:指向的规则):

图16:规则树

上下文树如下(节点名:指向的规则节点):

图17:上下文树

假如我们解析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的规范,级联顺序为(从低到高):

  1. 浏览器的声明
  2. 用户的一般声明
  3. 作者的一般声明
  4. 作者的important声明
  5. 用户的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 全局布局和增量布局

布局在整棵渲染树上触发时,称为全局布局,下面两种情况可能发生全局布局:

  1. 一个全局的样式改变影响所有的渲染对象,比如:font-size的改变。
  2. 窗口resize

布局也可以是增量进行的,这样只有标记为“dirty”的渲染对象会重新布局(也会导致一些额外的布局)。增量布局会在渲染对象为“dirty”的时候(异步)触发。例如,当网络接收到新的内容并添加到DOM树后,新的渲染对象会添加到渲染树中。

图18:增量布局,只有dirty的渲染对象和它们的children需要重新布局

2.6.3 异步布局和同步布局

增量布局是异步完成的。Firefox为增量布局生成了“reflow”队列以及一个调度器触发这些批处理命令。Webkit也有一个计时器用来执行增量布局——遍历树并为“dirty”状态的渲染对象重新布局。此外,当脚本请求样式信息时,例如:offsightHeight,会同步地触发增量布局。全局布局一般都是同步触发的。有的时候布局会被作为一个初始布局的回调,因为一些属性发生了改变,比如滚动条的位置发生改变。

2.6.4 优化

当一个布局因为resize或渲染位置(不是大小)的改变而触发时,渲染对象的大小将会从缓存中读取,而不会重新计算。某些情况下,如果只有子树被修改了,则布局并不从根开始。这种情况可能发生,比如变化发生在元素自身并且不影响它周围元素,例如将文本插入文本框中(否则每次击键都将触发从根开始的重排)。

2.6.5 布局过程

布局通常有以下几种模式:

  1. Parent渲染对象决定它的宽度。
  2. Parent渲染对象读取children,并且:
  3. 放置child渲染对象(设置它的x和y)。
  4. 在需要时(它们当前为“dirty”或处于全局布局状态下或是其他原因)调用child渲染对象的layout,这将计算child的高度。
  5. Parent渲染对象使用child渲染对象的累积高度以及margin和padding的高度来设置自己的高度——这将被parent渲染对象的parent使用。
  6. 将它的“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定义了绘制过程的顺序,这实际上是元素压入堆栈上下文的顺序,这个顺序影响着绘制,因为堆栈是从后向前绘制。一个块渲染对象的堆栈顺序是:

  1. background color
  2. background image
  3. border
  4. children
  5. 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区域。

图19:CSS2盒模型

每个节点生成0到n个这样的box,所有的元素都有一个display属性,用来决定它们生成box的类型,例如:

block  - 生成块状block
inline - 生成一个或多个行内block
none   - 不生成block

默认是inline,但是浏览器样式设置了其他默认值。例如,div元素默认是display: block;,你可以访问这里查看更多的默认样式表例子。

2.10.3 定位策略(Positioning scheme)

这里有三种定位策略:

  1. normal - 对象根据它在文档中的位置来定位,这意味着它在渲染树和DOM树中的位置是一致的,并根据它的盒模型和大小进行布局
  2. float - 对象先像正常的流一样布局,然后尽可能的向左或向右移动
  3. absolute - 对象在渲染树中的位置和DOM树中的位置无关

定位策略是通过设置position属性和float属性来实现的:

  • staticrelative会导致normal定位
  • absolutefixed会导致absolute定位

static定位中,不定义位置而使用默认的位置。在其他策略中,作者指定位置——top, bottom, left, right

Box布局的方式由这几项决定:

  • Box类型
  • Box大小
  • 定位策略
  • 扩展信息(比如图片的大小和屏幕尺寸)

2.10.4 Box 类型

Block Box:构成一个块,在浏览器窗口上有自己的矩形。

图20:Block Box

Inline Box:并没有自己的块状区域,包含在一个块状区域内。

图21:Inline Boxes

block是一个挨着一个垂直格式化,inline则在水平方向上格式化。

图22:Block and Inline formatting

Inline boxes放置在行内 box中,每行至少和最高的box一样高,当box以baseline对齐时,即一个元素的底部和另一个box上除底部以外的某点对齐,行高可以比最高的box高。当容器宽度不够时,行内元素将被放到多放中,这在p元素中经常发生。

图23:Lines

2.10.5 定位(Positioning)

2.10.5.1 Relative

相对定位,先按照一般的定位,然后按所要求的差值移动。

图24:相对布局

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>

将显示为:

图25:浮动布局

2.10.5.3 Absolute and fixed

这种情况下的布局完全不顾普通的文档流,元素不属于文档流的一部分,大小取决于容器。在Fixed布局中,容器就是viewport(可视区域)。

图26:固定布局

注意: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>
图27:固定布局

虽然绿色div排在红色div后面,可能在正常流中也已经被绘制在后面,但z-index有更高的优先级,所以在根box的堆栈中更靠前。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容