本文翻译自:web.dev/howbrowsers… (谷歌工程师Paul Irish整理的)
文章的原始来源:taligarsiel.com/Projects/ho… (作者:以色列开发者Tali Garsiel)
注:本文的发布时间是在2011年,所以有些约定,HTML 和 CSS 标准规范可能已经更新或者修改,仅作学习,文章很长,有些翻译可能不到位,见谅,也欢迎大家指出错误的地方~
前言
这本关于WebKit和Gecko内部运作的综合入门书,是以色列开发者Tali Garsiel的大量研究结果。几年来,她查阅了所有关于浏览器内部机制的公开数据,并花了大量时间阅读网络浏览器源代码。她写道:
在IE占主导地位的年代,除了将浏览器视为一个 "黑匣子 "外,没有什么可做的,但现在,随着开源浏览器的使用份额超过一半,现在是一个窥探引擎盖下面以及看看网络浏览器内部有什么的好时机。好吧,里面的东西是数以百万计的 C++ 代码......
Tali 在她的网站上发表了她的研究成果,但我们知道它应该有更多的读者,所以我们把它整理了一下,在这里重新发布。
作为一名网络开发者,学习浏览器的内部操作有助于你做出更好的决策,并了解开发最佳实践背后的理由。虽然这是一份相当长的文件,但我们建议你花一些时间去了解,我们保证你将会乐于这样做。
Paul Irish, Chrome Developer Relations
介绍
网络浏览器是我们使用最广泛的软件。在这篇入门文章中,我将解释它们在幕后是如何工作的。我们将看到当你在地址栏中输入google.com时发生了什么,直到你在浏览器屏幕上看到谷歌页面。
我们将讨论的浏览器
目前在桌面上使用的主要浏览器有五种。Chrome、Internet Explorer、Firefox、Safari和Opera。在移动端领域,主流的浏览器有安卓浏览器、iPhone、Opera Mini和Opera Mobile、UC浏览器、诺基亚S40/S60浏览器和Chrome,除了Opera浏览器之外,所有这些浏览器的内核都是基于WebKit的。我将举出开源浏览器Firefox和Chrome,以及Safari(部分开源)的例子。根据StatCounter的统计(截至2013年6月),Chrome、Firefox和Safari占据了全球桌面浏览器使用量的71%左右。在移动端,安卓浏览器、iPhone和Chrome浏览器构成了约54%的使用率。
浏览器的主要功能
浏览器的主要功能是通过从服务器上请求网络资源并将其显示在浏览器窗口中,来呈现你所选择的网络资源。该资源通常是一个HTML文档,但也可能是一个PDF、图像或其他类型的内容。资源的位置是由用户使用URI(统一资源标识符)指定的。
浏览器解析和显示HTML文件的方式是在HTML和CSS规范中规定的。这些规范由W3C(万维网联盟)组织维护,该组织的web的标准组织。多年来,浏览器只符合规范的一部分,它们开发了自己的扩展。这给 web 工作者带来了严重的兼容性问题。今天,大多数浏览器都只是或多或少地遵守了这些规范。
浏览器的用户界面有很多共同之处。其中共同的用户界面元素包括:
- 用于输入URI的地址栏
- 后退 和 前进 按钮
- 书签选项
- 用于 刷新 和 停止 加载当前文件的按钮
- 带你回到主页的按钮
奇怪的是,浏览器的用户界面并没有在任何正式的规范中规定,它只是来自多年经验形成的良好实践,以及浏览器之间的相互模仿。HTML5规范并没有定义浏览器必须具备怎么样的用户界面元素,但列出了一些常见的元素。其中包括地址栏、状态栏和工具栏。当然,也有一些特定浏览器特有的功能,如Firefox的下载管理器。
浏览器的顶层结构
浏览器的主要组成部分是:
用户界面:包括地址栏、后退/前进按钮、书签菜单等。除了你看到请求的页面的窗口,浏览器显示每一个部分属于用户界面。
浏览器引擎:在用户界面和渲染引擎之间传送指令。
渲染引擎:它负责显示请求的内容。例如,如果我们请求的内容是HTML,渲染引擎会解析HTML和CSS,并将解析后的内容显示在屏幕上。
网络:用于网络调用,如HTTP请求,独立于平台接口后面(与平台无关),为不同平台使用不同的实现。
用户界面后台:用于绘制combo boxes和window等基本部件。这个后台公开了一个与平台无关的通用接口,在底层使用操作系统提供的用户接口方法。
JavaScript解释器:用来解析和执行JavaScript代码。
数据存储:这是一个持久层。浏览器可能需要在本地保存各种数据,如cookie。浏览器也支持存储机制,如localStorage、IndexedDB、WebSQL和FileSystem。
Figure 1 : Browser components
需要注意的是,像Chrome这样的浏览器会运行多个渲染引擎实例:每个标签都有一个。每个标签在一个单独的进程中运行。
渲染引擎
渲染引擎的职责嘛...当然是“渲染”了,也就是在浏览器屏幕上显示所请求的内容。
默认情况下,渲染引擎可以显示HTML和XML文档和图像。它可以通过插件或扩展来显示其他类型的数据;例如,使用PDF查看器插件来显示PDF文档。然而,在本章中,我们将集中讨论其主要用途:显示使用CSS格式化的HTML和图像。
渲染引擎
不同的浏览器使用不同的渲染引擎:Internet Explorer使用Trident,Firefox使用Gecko,Safari使用WebKit。Chrome和Opera(从15版开始)使用Blink,这是WebKit的一个分支。
WebKit是一个开源的渲染引擎,最初是为Linux平台设计的引擎,后来被苹果公司修改,以支持Mac和Windows。更多细节请见webkit.org。
主流程
渲染引擎将开始从网络层获取所请求文件的内容。这通常会以8kB的块(chunks)来完成。
之后,这就是渲染引擎的基本流程。
Figure 2 : Rendering engine basic flow
渲染引擎将开始解析HTML文档,并将元素转换为一个叫做 "内容树" 的DOM节点。该引擎将解析样式数据,包括外部CSS文件和<style>中的样式。样式信息和HTML中的视觉指令将被用来创建另一棵树:渲染树。
渲染树包含具有颜色和尺寸等视觉属性的矩形。这些矩形按照正确的顺序显示在屏幕上。
在构建渲染树之后,它要经过一个 "layout"(布局)过程。这意味着给每个节点提供它应该出现在屏幕上的准确坐标。下一个阶段是painting(绘制)-- 渲染树将被遍历,每个节点将使用UI backend layer进行绘制。
重要的是,要明白这是一个渐进的过程。为了获得更好的用户体验,渲染引擎会尝试尽快在屏幕上显示内容。它不会等到所有的HTML都被解析完,才开始构建和布局渲染树。部分内容会先被解析并显示出来,而这个过程中会继续处理不断来自网络的其他内容。
主流程实例子
Figure 3 : WebKit main flow
Figure 4 : Mozilla's Gecko rendering engine main flow
从图3和图4可以看出,尽管WebKit和Gecko使用的术语略有不同,但流程基本一致。
Gecko 将视觉格式化的元素树称为 "Frame tree"("框架/骨架树")。每个元素都是一个框架。WebKit 使用术语 "Render Tree",它由 "Render Objects"组成。WebKit 使用术语 "layout "来定位元素,而 Gecko 称之为 "Reflow"。"Attachment "是 WebKit 的术语,用于连接DOM节点和视觉信息来创建渲染树。一个细微非语义上的差别是Gecko在HTML和DOM树之间有一个额外的层。它被称为 "content sink",是一个制造DOM元素的工厂。以下我们将会讨论流程的每个部分:
解析 - 概要
由于解析是渲染引擎中一个非常重要的过程,我们将对它进行更深入的研究。首先,我们对解析做一个简要的介绍。
解析一个文档意味着将其翻译成代码能够使用(识别)的结构。解析的结果通常是一棵代表文档结构的节点树。这被称为解析树或语法树。
例如,解析表达式 2 + 3 - 1 可以返回这个树。
图:数学表达式节点树
语法
解析是基于文件所遵循的语法规则:比如所使用的 语言 或 格式。你可以解析的每一种具有有特定的语法的格式,它由词汇和语法规则组成。它被称为无语境语法(与上下文无关)。人类语言不是这样的语言,因此不能用传统的解析技术进行解析。
解析-词法分析 联合体
解析可以分为两个子过程:词法分析和语法分析。
词法分析是将输入内容分解为tokens的过程。Tokens是语言词汇:有效构件的集合。在我们人类语言中,它包括所有出现在该语言的字典中的词。
语法分析是对语言语法规则的应用。
解析过程通常在两个组件之间进行处理:
-
词法分析器(有时称为标记生成器)负责将输入内容分解为有效标记(tokens)(有时称为 tokenizer)。 -
解析器负责根据语言的语法规则分析文档结构,从而构建解析树。
词法分析器知道如何剥离不相关的字符,如白色空格和换行符。
Figure 5 : from source document to parse trees
解析过程是反复进行的。解析器通常会向词法分析器询问一个新的token(标记),并尝试将该token与某个语法规则相匹配。如果一个规则被匹配,那么与该token相对应的节点将被添加到解析树中,然后解析器再次要求提供下一个token。
如果没有匹配的规则,解析器将在内部存储token,并继续询问tokens直到找到与所有内部存储的token匹配的规则。如果没有找到规则,那么解析器将引发一个异常。这意味着该文件无效,其中包含语法错误。
翻译
在许多情况下,解析树并不是最终产物。解析通常被用于翻译:将输入的文件转换为另一种格式。编译就是一个例子,编译器把源代码编译成机器码的过程,首先是将其解析成一个解析树,然后将该树翻译成机器码文件。
Figure 6 : 汇编流程
解析示例
在图5中,我们从一个数学表达式建立了一个解析树。让我们试着定义一个简单的数学语言,看看解析的过程。
关键术语
我们的语言(数学语言)可以包括整数、加号和减号。
语法:
- 构成语言的语法单位是表达式、项和运算符。
- 我们的语言可以包括任何数量的表达式。
- 表达式被定义为:一个“项”接一个“运算符”,然后再接一个“项”。
- 运算符是一个加号或一个减号。
- 项是一个整数token或一个表达式
让我们来分析一下输入:2 + 3 - 1。
- 匹配规则的第一个子串是
2:根据第5条规则,它是一个项。 - 第二个匹配项是
2+3:这符合第三条规则:一个项接着一个运算符,再接着另一个项。 - 下一个匹配项只会在输入的末尾被命中。
2 + 3 - 1是一个表达式,因为我们已经知道2 + 3是一个项,所以我们有一个项后面是一个运算符,接着后面是另一个项。而2 + +不会匹配任何规则,因此是一个无效的输入。
词汇和语法的正式定义
词汇通常用正则表达式来表达。
例如,我们的语言将被定义为:
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
正如你所看到的,整数是由一个正则表达式定义的。
语法通常以一种叫做BNF的格式来定义。我们的语言将被定义为:
表达式 定义为: 项 运算符 项
运算符 定义为: + | -
项 定义为: 整数 | 表达式
我们说过,如果一种语言的语法是无语境语法,那么它就可以被常规分析器解析。无语境语法的一个直观定义是一个可以完全用BNF表达的语法。正式的定义详见 Wikipedia's article on Context-free grammar。
【译者注】:
这里可能有的同学会对 ”无语境语法“不太了解,我这里补充一下:
”无语境语法“,想表达的意思就是这种语言是静态的(是死的,不灵活的),就比如你写一个
<字符,就必须会有一个>对应,不让就不让你解析通过,程序直接中断。文章中也有指出,我们人类的语言,以及HTML语言,它们都不是一种“无语境语法”(BNF)的语言,它们是灵活的,多变的,与当前语境息息相关的。
大概的意思就是这样,如有表达不当,还请指出。
解析器的类型
有两种类型的解析器:自上而下的解析器和自下而上的解析器。一个直观的解释是,自上而下的解析器检查语法的高层次结构,并试图找到规则匹配。自下而上的解析器从输入开始,逐渐将其转化为语法规则,从低层次的规则开始,直到满足高层次的规则。
让我们看看这两类分析器将如何解析我们的例子。
自上而下的解析器将从更高层次的规则开始:它先将识别2 + 3为一个表达式。然后,再将2+3-1识别为一个表达式(识别表达式的过程是不断变化的,与其他规则相匹配,但起点是最高层次的规则)。
自下而上的分析器将扫描输入内容,直到有一个规则被匹配。然后,它将用该规则替换匹配的输入。这将一直持续到输入的结束。部分匹配的表达式被放在解析器的堆栈中。
| Stack | Input |
|---|---|
| 2 + 3 - 1 | |
| term | + 3 - 1 |
| term operation | 3 - 1 |
| expression | - 1 |
| expression operation | 1 |
| expression |
【译者注】: 这个表格,文章中貌似整理错了,输入列的最后一个表格应该是空的
文章的原始来源链接为:taligarsiel.com/Projects/ho…
这种自下而上的解析器被称为shift-reduce解析器,因为输入被移到了右边(想象一下,一个指针首先指向输入的起点,然后向右移动),并被逐渐还原成语法规则。
自动生成解析器
有一些工具可以生成一个解析器。你向它们提供你的语言的语法 -- 词汇和语法规则 -- 它们就会生成一个工作的解析器。创建一个解析器需要对解析有深刻的理解,而且手工创建一个优化的解析器并不容易,所以parser generators(解析器生成器)非常有用。
WebKit使用两个著名的parser generators(解析器生成器)。Flex用于创建词法分析器,而Bison用于创建解析器(你可能会碰到它们的名字是Lex和Yacc)。Flex的输入是一个包含tokens(标记/令牌)的正则表达式定义的文件。Bison的输入是BNF格式的语言语法规则。
HTML解析器
HTML解析器的工作就是将HTML token (令牌/标记) 解析为一棵解析树。
HTML语法定义
HTML的词汇和语法是由W3C组织创建的规范所定义的。
Not a context free grammar
正如我们在解析介绍中所看到的,或许我们可以使用BNF这样的格式来正式地定义语法。
不幸的是,所有传统的解析器论题(话题/题材)都不适用于HTML(我提出这些论题并不是为了好玩 -- 它们将被用于解析CSS和JavaScript)。HTML不能轻易地由解析器需要的无语境语法来定义。
有一种定义HTML的正式格式 -- DTD(文档类型定义)-- 但它不是一种无语境语法。
乍一看,这似乎很奇怪;HTML与XML相当接近。有很多可用的XML解析器。HTML有一个XML的变体--XHTML,那么他们之间有什么大的区别吗?
不同的是,HTML的方法更加 "宽容":它允许你省略某些标签(这些将被隐式添加),或者有时省略开始或结束标签,等等。总的来说,它是一种 "soft"(软)语法,与XML的僵硬和苛刻的语法相反。
这个看似很小的细节却有很大的不同。一方面,这是HTML如此受欢迎的主要原因:它原谅了你的错误,使web工作者的生活变得简单。另一方面,它又使得编写正式语法变得困难。因此,总的来说,HTML不能被传统的分析器轻易解析,因为它的语法不是无语境的。HTML不能被XML解析器解析。
HTML DTD
HTML的定义是采用DTD格式。这种格式是用来定义SGML家族的语言。该格式包含所有允许的元素、它们的属性和层次结构的定义。正如我们前面所看到的,HTML的DTD并没有形成一个无语境语法。
DTD有一些变体。严格模式只符合规范,但其他模式包含对过去浏览器使用的标记的支持。其目的是向后兼容旧的内容。目前的严格DTD如链接:www.w3.org/TR/html4/st…
DOM
输出树("解析树")是一棵DOM元素和属性节点的树。DOM是文档对象模型的简称。它是HTML文档对象的呈现,也是HTML元素与外界的接口,如JavaScript。
树的根部是 "Document" 对象。
DOM与标记几乎是一对一的关系。例如:
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
这个标记将被翻译成以下DOM树:
Figure : DOM tree of the example markup
与HTML一样,DOM是由W3C组织指定的。见 www.w3.org/DOM/DOMTR 。它是一个操作documents的通用规范。一个特定的模块描述了HTML的特定元素。HTML的定义可以在这里找到: www.w3.org/TR/2003/REC… 。
当我说树包含DOM节点时,我的意思是树是由实现DOM接口之一的元素构成的。浏览器使用具体的实现,这些实现具有浏览器内部使用的其他属性。
解析算法
正如我们在前几节看到的,HTML不能用常规的自上而下或自下而上的解析器进行解析。
原因是:
- 语言的宽容性。
- 事实上,浏览器具有传统的容错能力,支持众所周知的无效HTML的情况。
- 解析过程是可重入的(递归,循环解析)。对于其他语言来说,源码在解析过程中不会发生变化,但在HTML中,动态代码(如包含document.write()调用的脚本元素)可以添加额外的tokens,因此解析过程实际上修改了输入。
由于无法使用常规解析技术,浏览器创建了自定义解析器来解析HTML。
HTML5规范对解析算法进行了详细描述。该算法由两个阶段组成:标记化和树的构建。
标记化是词法分析,将输入的内容解析为多个token。在HTML token中,有开始标记(<)、结束标记(>)、属性名称和属性值。
标记器识别出token,将其交给树形构造器,并消耗下一个字符来识别下一个token,以此类推,直到输入的结束。
Figure : HTML parsing flow (taken from HTML5 spec)
标记化算法
该算法的输出是一个HTML token。该算法被表述为一个状态机。每个状态消耗输入流中的一个或多个字符,并根据这些字符更新下一个状态。该决策受到当前标记化状态和树状结构状态的影响。这意味着相同消耗的字符将产生不同的结果,以获得正确的下一个状态,这取决于当前的状态。这个算法太复杂了,无法完全描述,所以让我们看一个简单的例子,这将有助于我们理解这个原理。
基本例子 -- 对以下HTML进行标记化。
<html>
<body>
Hello world
</body>
</html>
初始状态是 "数据状态"。遇到<字符时,状态变为 “Tag open state”。消耗一个a-z字符会导致创建一个 "Start tag token",状态被改变为 "Tag name state" 。我们一直处于这个状态,直到>字符被消耗掉。每个字符都被添加到新的token名称上。在我们的例子中,创建的标记是一个html token。
当到达>标签时,当前的标记被发射出来(处理完成),状态变回 "数据状态"。<body>标签将以同样的步骤处理。到目前为止,html和body标签(指各自的开始标签)已经被发射出去(处理完成)了。我们现在回到了 "数据状态"。消耗Hello world的H字符将导致创建和发射一个字符token,这一直持续到</body>的<。我们将为Hello world的每个字符发射一个字符token。
我们现在回到了 “Tag open state”。消耗下一个输入/将导致创建一个结束的标签token,并移动到 "Tag name state"。我们再次停留在这个状态,直到我们到达>。然后新的标签token将被发射出来,我们回到 "数据状态"。</html>的输入的处理方式与之前的情况一样。
Figure : Tokenizing the example input
树的构建算法
当创建解析器时,Document对象也被创建了。在树构造阶段,以Document为根的DOM树将被修改,元素将被添加到其中。每个由标记器发出的节点将被树构建器处理。对于每个token,规范定义了与之相关DOM元素,并将为该token创建DOM元素。该元素被添加到DOM树中,同时也添加到开放元素的堆栈中。这个堆栈被用来纠正嵌套不匹配和未关闭的token。该算法也被描述为一个状态机。这些状态被称为 "插入模式"。
让我们来看看示例输入的树构造过程:
<html>
<body>
Hello world
</body>
</html>
树状结构阶段的输入是来自标记化阶段的一串token。第一个模式是 "初始模式"。当收到 "html "token 将会移动到 "before html " 模式,并在该模式下继续处理该token。进而将会引起HTMLHtmlElement元素的创建,它将被添加到(appended to)根Document对象上。
状态将被改变为 "before head"。接着收到"body" token。尽管在上面例子中我们没有 "head" token,但一个HTMLHeadElement将被隐式地创建,并被添加到树中。
我们现在进入 "in head" 模式,然后再到 "after head"。body token被重新处理,一个HTMLBodyElement被创建并插入,模式被转移到 "in body"。
现在收到 "Hello world "字符串的字符令牌(tokens)。第一个字符引起创建并插入一个 "Text"节点,其他字符将被添加到该节点上。
收到body的结束token将引起转移到 "after body "模式。现在我们将收到html结束token,这将使我们进入 "after after body "模式。接收到文件结束token将结束解析工作。
Figure : tree construction of example html
解析结束后的动作
在这个阶段,浏览器将把document标记为交互式(活跃状态),并开始解析处于 "defer"模式的Script脚本:那些应该在文档被解析后执行的脚本。 然后,document的状态将被设置为 "complete",一个 "load "事件将被触发。
浏览器的错误容忍度
你永远不会在HTML页面上得到一个 "无效语法 "的错误。浏览器会修复任何无效的内容并继续前进。
以这个HTML为例:
<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>
我肯定违反了大约一百万条规则("mytag "不是一个标准标签,"p "和 "div "元素嵌套错误等等),但浏览器仍能正确显示,并且没有抱怨。所以分析器的很多代码都是在修复HTML开发者的错误。
错误处理在浏览器中是相当一致的,但令人惊讶的是,它还没有成为HTML标准规范的一部分。就像书签和后退/前进按钮一样,它只是多年来在浏览器中发展起来的东西。有一些已知的无效的HTML结构在许多网站上重复出现,而浏览器则试图以符合其他浏览器的方式来修复它们。
HTML5 规范确实定义了其中的一些要求。(WebKit在HTML解析器类开头的注释中对此进行了很好的总结)。
解析器将标记化的输入(token)解析到文档中,建立起文档树。如果文档是格式良好的,解析它是很简单的。
不幸的是,我们必须处理许多格式不规范的HTML文档,所以分析器必须对错误有一定的容忍度。
我们至少要照顾(考虑)到以下错误情况:
- 我们添加的元素在一些外部标签中被明确禁止。在这种情况下,我们应该关闭所有直到禁止该元素的那个标签,然后再添加它。
- 我们不允许直接添加元素。这可能是写文档的人忘记了中间的一些标签(或者中间的标签是可选的)。下面的标签就可能是这种情况。HTML HEAD BODY TBODY TR TD LI(我是不是忘了什么?)
- 我们想在一个行内元素里面添加一个块状元素。关闭所有行内元素,直到下一个更高的块状元素。
- 如果这没有帮助,就关闭元素,直到我们被允许添加元素 -- 或者忽略这个标签。
让我们来看看一些WebKit的容错实例:
</br> instead of <br>
有些网站使用</br>而不是<br>。为了与IE和Firefox兼容,WebKit将其视为
。
代码:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
注意,错误处理是内部进行的:它不会呈现给用户。
游离的table
游离表是指在另一个表内部的表,但不在一个表的单元格内。
例如:
<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);
WebKit对当前元素的内容使用了一个堆栈:它将把内部的表从外表的堆栈中弹出。现在,这些表将是相邻的关系了。
嵌套的表单元素
如果用户把一个表格放在另一个表格里面,第二个表格会被忽略。
代码:
if (!m_currentFormElement) {
m_currentFormElement = new HTMLFormElement(formTag, m_document);
}
标签层次结构太深
注释不言自明。
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。
CSS解析
还记得介绍章节中的解析概念吗?嗯,与HTML不同,CSS是一种无上下文(无语境)的语法,可以使用介绍章节中描述的分析器类型进行解析。事实上,CSS规范中定义了CSS的词法和语法。
最新的CSS规范:www.w3.org/TR/CSS/#css
我们来看一些例子: 词法语法(词汇)由每个token的正则表达式来定义。
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*] ')' ]
;
解释:
一个规则集就是这种结构。
div.error, a.error {
color:red;
font-weight:bold;
}
div.error和a.error是选择器。大括号内的部分包含这个规则集所应用的规则。这个结构在这个定义中被正式定义。
ruleset
: selector [ ',' S* selector ]*
'{' S* declaration [ ';' S* declaration ]* '}' S*
;
这意味着一个规则集是一个选择器或可选的若干选择器,由逗号和空格(S代表空白)分隔。一个规则集包含大括号,在大括号内是一个声明或可选的若干个声明,用分号隔开。"声明 "和 "选择器 "将在下面的BNF定义中定义。
WebKit CSS 解析
WebKit使用Flex and Bison解析器生成器来从CSS语法文件中自动创建解析器。正如你在解析器介绍中所回忆的,Bison创建了一个自下而上的移位还原解析器。Firefox使用的是手动编写的自上而下的解析器。在这两种情况下,每个CSS文件都被解析为一个StyleSheet对象。每个对象都包含CSS规则。CSS规则对象包含选择器和声明对象以及与CSS语法对应的其他对象。
处理脚本和样式表的顺序
Scripts
web的模式是同步的。作者希望当解析器到达<script>标签时,脚本能立即被解析和执行。文档的解析会停止,直到脚本被执行。如果脚本是外部的,那么必须首先从网络上获取资源--这也是同步进行的,解析也会停止,直到资源被获取。这是多年来的模式,在HTML4和5规范中也有规定。作者可以为脚本添加 "defer"属性,在这种情况下,它不会停止文档解析,而是在文档被解析后执行。HTML5增加了一个选项,可以将脚本标记为异步的,因此它将由不同的线程来解析和执行。
推测性解析
WebKit和Firefox都做了这种优化。在执行脚本的同时,另一个线程会解析文档的其余部分,找出需要从网络上加载的其他资源,并加载它们。通过这种方式,资源可以在并行连接上加载,整体速度得到提高。
注意:推测性解析器不修改DOM树,这项工作(修改DOM树)是由主解析器来处理,推测性解析器只解析对外部资源的引用,如外部脚本、样式表和图片等。
【译者注】: 上述提到的,估计是我们日常在html文件头部的head标签内,所声明的meta标签中的
prefetch和preload的特性。在MDN文档中,称之为 “预加载扫描器”,详情见:developer.mozilla.org/zh-CN/docs/…
如有表达不当,还请指出。
样式表
另一方面,样式表有一个不同的模式。从概念上看,既然样式表不改变DOM树,就没有理由等待它们并停止文档的解析。不过有一个问题,就是脚本在文档解析阶段要求提供样式信息。如果样式还没有被加载和解析,脚本会得到错误的答案,显然这引起了很多问题。这似乎是一个边缘案例,但却很常见。当有一个样式表还在加载和解析的时候,Firefox会阻止所有的脚本。而WebKit只在脚本试图访问某些可能受未加载的样式表影响的样式属性时才会阻止它们。
渲染树的构建
在构建DOM树的同时,浏览器还构建了另一棵树,即渲染树。这棵树是由视觉元素组成的,并按照它们将被显示的顺序排列。它是文档的可视化表示。这棵树的目的是为了能够按照正确的顺序绘制内容。
Firefox 将渲染树中的元素称为 "框架/骨架"(“Frames”)。WebKit 使用渲染器或渲染对象这一术语。
渲染器知道如何布局和绘制自己及其子代。
WebKit 的 RenderObject 类是渲染器的基类,它有如下定义。
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属性来决定应该为一个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;
}
元素的类型也要考虑:例如,表单控件和表格有特殊的框架/骨架。
在WebKit中,如果一个元素想要创建一个特殊的渲染器,它将重写createRenderer()方法。渲染器指向包含非几何信息的样式对象。
渲染树与DOM树的关系
渲染器对应于 DOM 元素,但这种关系并不是一对一的。非可视化的 DOM 元素不会被插入到渲染树中。一个例子如html文件中的"head"元素。另外,display值被指定为 "none"的元素也不会出现在树中(而具有 "hidden"visibility的元素会出现在树中)。
有一些DOM元素对应着几个视觉对象。这些通常是具有复杂结构的元素,不能用一个矩形来描述。例如,"select"元素有三个渲染器:一个用于显示区域,一个用于下拉列表框,一个用于按钮。另外,当文本因宽度不足以容纳一行而被分成多行时,新的一行将被添加为额外的渲染器。
多重渲染器的另一个例子是块状的HTML。根据CSS规范,一个行内元素必须只包含块元素或只包含行内元素。在混合内容的情况下,将创建匿名的块渲染器来包裹行内元素。
【译者注】: 上面这句话感觉怪怪的,暂时没法理解
应该在新的CSS规范有更新一些不同的体现,请移步:www.w3.org/TR/CSS/#mod…
一些渲染对象对应于一个DOM节点,但不在树中的同一位置。浮动和绝对定位的元素脱离了常规文档流,被放置在树的不同部分,并被映射到真正的框架。在它们本该在的地方设置一个占位框
图:渲染树和相应的DOM树。"Viewport"是初始的包含块。在WebKit中,它将是 "RenderView"对象。
构建树的流程
在Firefox中,展示层被注册为DOM更新的监听器。展示层将框架的创建委托给FrameConstructor,该构造器解析样式(见样式计算)并创建了一个框架。
在WebKit中,解析样式和创建渲染器的过程被称为 "attachment"。每个DOM节点都有一个 "attach"方法。attachment是同步的,节点插入到DOM树中会调用新节点的 "attach"方法。
处理html和body标签的结果就是构建渲染树的根。根渲染对象对应于CSS规范中所说的包含块:包含所有其他块的最上面的块。它的尺寸是视口:浏览器窗口显示区域的尺寸。Firefox称其为ViewPortFrame,WebKit称其为RenderView。这就是文档所指向的渲染对象。树的其余部分是以DOM节点插入的方式构建的。
详见 the CSS2 spec on the processing model.
样式计算
建立渲染树需要计算每个渲染对象的视觉属性。这是通过计算每个元素的样式属性来实现的。
样式包括各种来源的样式表、内联样式元素和HTML中的视觉属性(如 "bgcolor"属性)。后者被翻译成匹配的CSS样式属性。
样式表的来源是浏览器的默认样式表,页面作者提供的样式表和用户样式表 -- 这些是由浏览器用户提供的样式表(浏览器让你定义自己喜欢的样式。例如,在Firefox中,这是通过在 "Firefox Profile" 文件夹中放置一个样式表来实现的)。
样式计算带来了一些困难:
-
样式数据是一个非常大的结构,容纳了众多的样式属性,这可能会导致内存问题。
-
如果不进行优化,为每个元素寻找匹配的规则会导致性能问题。为每个元素遍历整个规则列表来寻找匹配是一项繁重的任务。选择器可以有复杂的结构,这可能导致匹配过程开始于一个看似有希望的路径,但被证明是徒劳的,必须尝试另一个路径。
例如 -- 这个复合选择器:
div div div div{ ... }意味着规则适用于一个
<div>,他是3个div的后代。假设你想检查该规则是否适用于某个<div>元素。你选择树上的某个路径进行检查。你可能需要遍历整个节点树,然后发现只有两个div,规则不适用。然后你需要尝试树上的其他路径。 -
应用这些规则涉及到相当复杂的级联规则,这些规则定义了规则的层次结构。
让我们看看浏览器如何面对这些问题的:
共享样式数据
WebKit节点引用样式对象(RenderStyle)。这些对象在某些条件下可以被节点共享。节点是兄弟姐妹或表(堂)兄弟姐妹:
- 这些元素必须处于相同的鼠标状态(例如,不允许一个处于
:hover状态,而另一个则不是)。 - 两个元素之间都不应该带有id选择器
- 标签名称必须一致
- 类属性必须一致
- 映射属性集必须相同
- link状态必须一致
- focus状态必须一致
- Neither element should be affected by attribute selectors, where affected is defined as having any selector match that uses an attribute selector in any position within the selector at all (【译者注】:不知怎么翻译好......)
- 元素上必须没有内联样式属性
- 必须完全没有使用相邻选择器。当遇到任何同级选择器时,WebCore会简单地抛出一个全局开关,并在它们出现时禁用整个文档的样式共享。这包括
"+"以及:first-child和:last-child选择器
Firefox 规则树
Firefox有两个额外的树,以便更容易进行样式计算:rule树和style context树。WebKit也有样式对象,但它们并不像style context树那样存储在一个树中,只有DOM节点指向其相关的样式。
Figure : Firefox style context tree.
样式上下文包含终端值。这些值是通过正确的顺序应用所有的匹配规则,并执行将它们从逻辑值转化为具体值的操作来计算的。例如,如果逻辑值是屏幕的百分比,它将被计算并转换为绝对单位。规则树的想法确实很聪明。它能够在节点之间共享这些值,以避免再次计算它们。这也节省了空间。
所有匹配的规则都存储在一棵树上。路径中最下面的节点有更高的优先级。这棵树包含了所有被发现的规则匹配的路径。储存规则是懒散地进行的。树不是一开始就为每个节点计算的,但每当需要计算一个节点的样式时,计算的路径就被添加到树中。
我们的想法是把树的路径看作是词库中的词。假设我们已经计算了这个规则树:
假设我们需要为内容树中的另一个元素匹配规则,并发现匹配的规则(按正确的顺序)是B-E-I。因为我们已经计算了路径A-B-E-I-L,在树上已经有了这个路径,所以这个是时候我们匹配B-E-I花的成本就更少了。
让我们看看这棵树是如何为我们节省工作成本的。
结构划分
样式上下文被划分为结构。这些结构包含某个类别的样式信息,如 边框 或 颜色 。一个结构中的所有属性要么是继承的,要么是非继承的。继承的属性是指:除非由元素自身定义,否则会继承其父级的属性。非继承属性(称为 "重置 "属性)如果没有定义,则使用默认值。
树通过在树中缓存整个结构(包含计算的端值)来帮助我们。我们的想法是,如果底部节点没有提供结构的定义,可以使用上部节点中的缓存结构。
使用规则树计算样式上下文
在计算某个元素的样式上下文时,我们首先在规则树中计算一个路径,或者使用现有的路径。然后我们开始应用路径中的规则来填充我们新的样式上下文中的结构。我们从路径的最底层节点开始,即优先级最高的节点(通常是最具体的选择器),然后向上遍历规则树,直到我们的结构被填满。如果该规则节点中没有关于该结构的规范,那么我们可以大大优化--我们沿着树向上走,直到找到一个完全规范的节点,然后简单地指向它--这是最好的优化--整个结构是共享的。这就节省了端值的计算和内存。
如果我们找到了部分定义,我们就沿着树往上走,直到结构被填满。
如果我们没有找到结构的任何定义,那么在结构是 "继承 "类型的情况下,我们会指向上下文树中的父结构。在这种情况下,我们也成功实现了结构的共享。如果是重置的结构,那么将使用默认值。
如果最具体的节点确实增加了数值,那么我们需要做一些额外的计算,将其转化为实际数值。然后我们将结果缓存在树形节点中,这样它就可以被子节点使用。
如果一个元素有一个兄弟姐妹,指向同一个树节点,那么整个样式上下文可以在它们之间共享。
让我们看一个例子。假设我们有这样一个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">another error</div>
</body>
</html>
以下是CSS规则:
div {margin: 5px; color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}
为了简化事情,我们假设我们只需要填写两个结构:颜色结构和边距结构。颜色结构只包含一个成员:颜色;边距结构包含四个边。
由此产生的规则树将看起来像这样(节点上标有节点名称:它们所指向的规则的编号):
Figure : The rule tree
假设我们解析了HTML,找到了第二个<div>标签。我们需要为这个节点创建一个样式上下文并填充其样式结构。
我们将匹配规则,发现<div>的匹配规则是1、2和6。这意味着树中已经有一个现有的路径,我们的元素可以使用,我们只需要为规则6添加一个节点(规则树中的节点F)。
我们将创建一个样式上下文并把它放在上下文树中。新的样式上下文将指向规则树中的节点F。
现在我们需要填充样式结构。我们将从填充 margin 结构开始。由于最后一个规则节点(F)没有添加到边距结构中,我们可以沿着树形向上走,直到找到在之前的节点插入中计算的缓存结构并使用它。我们将在节点B上找到它,该节点是指定保证金规则的最上层节点。
我们确实有一个颜色结构的定义,所以我们不能使用缓存的结构。由于颜色有一个属性,我们不需要再去树上填充其他属性。我们将计算终端值(将字符串转换为RGB等),并在此节点上缓存计算出的结构。
第二个<span>元素的工作就更简单了。我们将对规则进行匹配,并得出结论,即它与前一个span一样,指向规则G。由于我们有指向同一节点的兄弟姐妹,我们可以共享整个样式上下文,只需指向前一个跨度的上下文即可。
对于包含从父级继承的规则的结构,缓存是在上下文树上进行的(颜色属性实际上是继承的,但Firefox将其视为重置并在规则树上缓存)。
例如,如果我们在一个段落中为字体添加了规则。
p {font-family: Verdana; font size: 10px; font-weight: bold}
那么作为上下文树中 div 的子代的段落元素就可能与他的父代共享同一个字体结构。这是在没有为该段落指定字体规则的情况下。
在没有规则树的WebKit中,匹配的声明被遍历了四次。首先应用非重要的高优先级属性(应该首先应用的属性,因为其他属性依赖于它们,例如显示),然后是高优先级的重要属性,然后是正常优先级的非重要属性,然后是正常优先级的重要规则。这意味着多次出现的属性将按照正确的级联顺序来解决。最后的赢家。
所以总结一下:共享样式对象(完全或其中的一些结构)解决了问题1和3。Firefox的规则树也有助于按照正确的顺序应用属性。
处理规则以更容易匹配
样式规则有几个来源:
- CSS规则,无论是在外部样式表还是在
<style>元素中。
p {color: blue}
- 内联样式属性,如
<p style="color: blue" />
- HTML视觉属性(被映射到相关的样式规则)。
<p bgcolor="blue" />
后两者很容易与元素相匹配,因为他拥有样式属性,而且HTML属性可以用元素作为key进行映射。
正如之前在第2个问题中指出的,CSS规则的匹配可能比较棘手。为了解决这个困难,对规则进行了处理,以方便使用。
在解析样式表之后,根据选择器将规则添加到多个散列映射之一。有按id、按类名、按标签名的映射,以及任何不属于这些类别的一般映射。如果选择器是一个id,规则将被添加到id映射中,如果它是一个类,它将被添加到类映射中等等。
这种操作使得匹配规则更加容易。没有必要查看每个声明:我们可以从映射中提取元素的相关规则。这种优化消除了95%以上的规则,因此在匹配过程中甚至不需要考虑它们(4.1)。
我们来看看下面的样式规则示例:
p.error {color: red}
#messageDiv {height: 50px}
div {margin: 5px}
第一条规则将被插入到类映射中。第二个插入id映射,第三个插入标签映射。
对于HTML片段:
<p class="error">an error occurred</p>
<div id=" messageDiv">this is a message</div>
我们将首先尝试找到 p 元素的规则。 类映射将包含一个“error”键,在该键下可以找到“p.error”的规则。 div元素在 id 映射(key是id) 和 标签映射 中会有相关的规则。 所以剩下的唯一工作就是找出由键提取的哪些规则真正匹配。
如果这个div的规则是:
table div {margin: 5px}
它仍然会从标签映射中提取,因为key是最右边的选择器,但它不会匹配这个的 div 元素,因为它没有一个 table 祖先。
WebKit和Firefox都做了这种操作(处理)。
按正确的级联顺序应用规则
样式对象有对应于每个视觉属性(所有CSS属性,但更通用)的属性。如果该属性没有被任何匹配的规则所定义,那么部分属性可以被父元素样式对象继承。其他属性有默认值。
当有一个以上的定义时,问题就开始了 -- 下面有解决这个问题的级联顺序。
样式表层叠顺序
一个样式属性的声明可以出现在多个样式表中,也可能在一个样式表中出现几次。这意味着应用这些规则的顺序是非常重要的。这就是所谓的 "级联 "顺序。根据CSS2规范,层叠顺序是(从低到高)。
浏览器声明是最不重要的,只有当声明被标记为重要时,用户才会覆盖作者。具有相同顺序的声明将先按专一性排序,然后按指定的顺序排序。HTML可视属性被转换为匹配的CSS声明。它们被视为具有低优先级的作者规则。
【译者注】:
这里的CSS声明是指:CSS的属性名值对,如color: red。
详见:CSS级联/层叠
特异性
选择器的特异性由CSS2规范定义如下:
- 如果来自的声明是一个'style'属性而不是一个带有选择器的规则,则计数1,否则为0 (= a)
- 计算选择器中ID属性的数量(= b)
- 计算选择器中其他属性和伪类的数量(= c)
- 计算选择器中的标签元素名称和伪元素的数量(= d)
将a-b-c-d这四个数字串联起来(在一个大基数的数字系统中),就得到了特定性。
您需要使用的基数由您在某个类别中拥有的最高计数定义。
例如,如果a=14,则可以使用十六进制。在a=17这种不太可能的情况下,您将需要一个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 */
规则分类
在规则被匹配后,它们会根据级联规则进行排序。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;
}
渐进过程
WebKit使用一个flag来标记所有顶层样式表(包括@imports)是否已经被加载。如果样式在附加(appending)时没有完全加载,则会使用占位符,并在文档中进行标记,一旦样式表被加载,它们将被重新计算。
Layout(布局)
当渲染器被创建并添加到树中时,它并没有位置和尺寸。计算这些值被称为布局(layout)或回流(reflow)。
HTML使用基于流式的布局模型,这意味着在大多数情况下,有可能一次性计算出几何图形。在 "流 "中较晚的元素通常不会影响在 "流 "中较早的元素的几何形状,因此布局可以在文档中从左到右、从上到下地进行。但也有例外:例如,HTML表格可能需要多于一次的处理。
坐标系统是相对于根框架(root frame)的。使用顶部和左侧坐标。
布局是一个递归过程。它从根渲染器开始,对应于HTML文档中的<html>元素。布局工作通过部分或全部的框架层次继续递归,为每个需要几何信息的渲染器计算几何信息。
根渲染器的位置是0,0,其尺寸是整一个视口 -- 浏览器窗口的可见部分。
所有的渲染器都有一个 "layout"或 "reflow"方法,每个渲染器都会调用其需要布局的子代layout方法。
Dirty bit system
为了不对每一个小的变化进行全面布局,浏览器使用了一个 "dirty bit "系统。一个被改变或添加的渲染器会将自己及其子代标记为 "dirty":需要进行布局。
这里有两个标志。"dirty",和 "children are dirty",这意味着尽管渲染器本身可能没有问题,但它至少有一个需要布局的孩子。
全局和增量布局
布局可以在整个渲染树上被触发 -- 这是 "全局"布局。这种情况可能会发生,因为:
- 一个影响所有渲染器的全局样式变化,比如字体大小的变化。
- 屏幕尺寸大小的改变。
布局可以是渐进式的,只有dirty的渲染器会被布局(这可能会造成一些负担,需要额外的布局)。
当渲染器变脏时,增量布局会被触发(异步)。例如,当来自网络的额外内容,被添加到 DOM树之后,新的渲染器被追加到渲染树中。
Figure : Incremental layout - only dirty renderers and their children are laid out (3.6)
异步和同步布局
增量布局是以异步方式进行的。Firefox为增量布局排队 "reflow commands"(回流命令),一个调度器会触发这些命令的批量执行。WebKit 也有一个执行增量布局的计时器 -- 树被遍历,"dirty"渲染器被布局出来。
要求提供样式信息的脚本,如 "offsetHeight" 可以同步触发增量布局。
全量布局通常会被同步触发。
有时布局是在初始布局后作为回调触发的,因为一些属性,如滚动位置改变了。
优化
"调整大小"(resize) 或 渲染器 位置的变化(不是尺寸)触发页面布局时,渲染器的尺寸会从缓存中获取,而不是重新计算。
在某些情况下,只有一个子树被修改,布局并不从根部开始。这可能发生在变化是局部的,不影响周围环境的情况下 -- 比如在文本字段中插入文本(否则每个按键都会触发从根开始的布局)。
布局过程
布局通常有以下模式:
- 父级渲染器决定自己的宽度。
- 父级对子代进行检查:
- 放置子渲染器(设置其X和Y)
- 如果需要的话,调用子布局 -- 它们是dirty的,或者我们是在一个全局布局中,或者出于其他原因 -- 这将计算出子的高度。
- 父级使用子级的
累积高度以及边距和填充的高度来设置自己的高度 -- 这将被父级渲染器的父级使用。 - 将其dirty bit设置为false。
Firefox使用一个 "状态 "对象(nsHTMLReflowState)作为布局(术语称为 "回流")的参数。在其他方面,该状态包括父级的宽度。
Firefox布局的输出是一个 "度量 "对象(nsHTMLReflowMetrics)。它将包含渲染器计算的高度。
宽度(width)计算
渲染器的宽度是通过容器块的宽度、渲染器的样式 "宽度"属性、边距和边框来计算的。
例如,下面这个div的宽度:
<div style="width: 30%"/>
会被WebKit计算为如下(class RenderBox method calcWidth):
- 容器的宽度是容器的availableWidth和0的最大值。在这种情况下,availableWidth是contentWidth,其计算方法是:
clientWidth() - paddingLeft() - paddingRight()
clientWidth和clientHeight代表一个对象的内部,不包括边框和滚动条。
- 元素的宽度是 "width" 样式属性。它将通过计算容器宽度的百分比,作为一个绝对值来计算。
- 现在已经添加了水平边框和内边距。
到目前为止,这是 "首选宽度" 的计算。现在将计算最小和最大的宽度。
如果首选宽度大于最大宽度,则使用最大宽度。如果它小于最小宽度(最小的不可破损的单位),则使用最小宽度。
这些值被缓存起来,以备需要布局时使用,但宽度不会改变。
断线(Line Breaking)
当布局中的某个渲染器决定需要中断时,该渲染器就会停止,并向布局的父级传播它需要中断的信息。父节点会创建额外的渲染器,并对它们调用布局。
Painting(绘制)
在绘制阶段,渲染树被遍历,渲染器的 "paint() "方法被调用以能够在屏幕上显示内容。绘制使用的是UI基础设施组件。
全量(全局)和增量
与布局一样,绘制也可以是全局的,即绘制整个树,也可以是增量的。在增量绘制中,部分渲染器的变化不会影响整个树。改变后的渲染器会使其在屏幕上的矩形失效。这导致操作系统将其视为一个 "脏区域"(dirty region),并产生一个 "paint"事件。操作系统做得很巧妙,把几个区域凝聚成一个。在Chrome浏览器中,情况更为复杂,因为渲染器是在与主进程不同的进程中。Chrome在某种程度上模拟了操作系统的行为。渲染器会监听这些事件,并将消息委托给渲染根。渲染树被遍历,直到到达相关的渲染器。它将重绘自己(通常还有包括它的子代)。
绘制顺序
CSS2 定义了绘制过程的顺序. 这实际上是元素在堆叠上下文中的堆叠顺序。这个顺序会影响到绘制,因为堆叠是从后往前绘制的。块状渲染器的堆叠顺序是:
- 背景颜色
- 背景图片
- 边框
- 子代
- 轮廓
Firefox 显示列表
Firefox会遍历渲染树,为所绘制的矩形建立一个显示列表。它包含了与该矩形相关的渲染器,并按照正确的绘制顺序(渲染器的背景,然后是边框等)。
这样一来,树的重绘只需要遍历一次,而不是多次 -- 先绘制所有的背景,再绘制所有的图像,然后绘制所有的边框等等。
Firefox优化这一过程,不添加会被隐藏的元素,比如完全在其他不透明元素下面的元素。
WebKit的矩形存储
在重绘之前,WebKit将旧的矩形保存为一个位图。然后,它只绘制出新旧矩形之间的差值。
动态变化
浏览器试图对一个变化做最小的动作。因此,元素颜色的变化只会导致该元素的重绘。元素位置的变化将导致该元素、其子元素和可能的同级元素的布局(layout)和重绘(repaint)。添加一个DOM节点将导致该节点的布局和重绘。重大的改变,比如增加 "html"元素的字体大小,将导致缓存的无效化、中继输出和整个树的重绘。
渲染引擎线程
渲染引擎是单线程的。除了网络操作之外,几乎所有的事情都发生在一个单线程中。在Firefox和Safari中,这是浏览器的主线程。在Chrome中,它是标签进程的主线程。
网络操作可以由几个并行线程执行。并行连接的数量是有限的(通常是2-6个连接)。
事件循环
浏览器的主线程是一个事件循环。它是一个无限的循环,保持进程的活跃。它等待事件(如布局和绘制事件)并处理它们。这是Firefox主事件循环的代码。
while (!mExiting)
NS_ProcessNextEvent(thread);
CSS2 视觉模型
canvas
根据CSS2规范,canvas一词描述的是 "渲染格式结构的空间":浏览器绘制内容的地方。
canvas对于空间的每个维度都是无限的,但浏览器会根据视口的尺寸选择一个初始宽度。
根据www.w3.org/TR/CSS2/zin… ,如果画布包含在另一个画布中,则是透明的,如果没有,则给定一个浏览器定义的颜色。
CSS 盒模型
CSS盒模型描述了为文档树中的元素生成的矩形框,并根据视觉格式化模型进行布局。
每个盒子都有一个内容区(如文本、图像等)和可选的 padding、border和margin区域。
Figure : CSS2 box model
每个节点都会生成0...n个这样的盒子。
所有元素都有一个 "display"属性,决定了将被生成的盒子的类型。
举例:
block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.
默认是内联,但浏览器样式表可以为元素设置其他默认值。例如:"div"元素的默认display是block。 你可以在这里找到一个默认样式表的例子:www.w3.org/TR/CSS2/sam… 。
定位方案
有三个方式:
- normal:对象是根据它在文档中的位置来定位的。这意味着它在渲染树中的位置就像它在DOM树中的位置一样,并根据它的盒子类型和尺寸来布局。
- Float:对象首先像正常流一样布局,然后尽可能向左或向右移动
- Absolute:该对象在渲染树中的位置与在DOM树中的位置不同
定位方案由“position”属性和“float”属性设置。
- static 和 relative 是一个正常流
- absolute 和 fixed 为 绝对定位
在static定位中,没有定义位置,使用的是默认定位。在其他方案中,作者可以指定了位置:上、下、左、右。
盒子的布局方式由以下元素决定:
- 盒子的类型
- 盒子的尺寸
- 定位方式
- 外部信息,如图像的大小 和 屏幕的大小
盒子的类型
块级盒子:形成一个块 -- 在浏览器窗口中有自己的矩形。
Figure : Block box
行内盒子:没有自己的块,而是包含在一个块的内部。
Figure : Inline boxes
块盒子的格式是垂直的,一个接一个。行内盒子是水平格式化的。
Figure : Block and Inline formatting
行内盒子被放在线或 "线框 "内。这些线至少和最高的盒子一样高,但也可以更高,当这些盒子以 "基线 "对齐时 -- 这意味着一个元素的底部对齐在另一个盒子的某一点上,而不是底部。如果容器宽度不够,内联将被放在几行上。这通常是发生在一个段落中的情况。
Figure : Lines
Positioning
Relative
相对定位 -- 像平常一样定位,然后按要求的 delta 移动。
Figure : Relative positioning
Floats
一个浮动盒子被移到一条线的左边或右边。有趣的是,其他的盒子会围绕它流动。HTML代码如下:
<p>
<img style="float: right" src="images/image.gif" width="100" height="100">
Lorem ipsum dolor sit amet, consectetuer...
</p>
看起来如:
Figure : Float
Absolute and fixed
布局的定义与正常文档流无关。元素不参与正常的文档流。尺寸是相对于容器的。在固定情况下,容器是视口。
Figure : Fixed positioning
即使我们滚动文档,fixed盒子也不会移动!
Layered representation(分层展示)
这是由 z-index CSS属性指定的。它表示盒子的第三个维度:它沿 "z轴 "的位置。
这些盒子被划分为堆栈(称为堆叠上下文)。在每个堆栈中,后面的元素将先被绘制,前面的元素在最上面,更接近用户。在重叠的情况下,最前面的元素将隐藏前面的元素。
堆栈是根据z-index属性来排序的。具有 "z-index "属性的盒子形成一个本地堆栈。视口有外部堆栈。
示例:
<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>
效果如下:
Figure : Fixed positioning
上述代码中尽管红色 div 在 绿色 div 在上方,并且在常规文档流中比 绿色div 先绘制,但其 z-index 属性优先级更高,因此它在根盒子所持有的堆栈中更靠前。
资源
-
浏览器架构
- Grosskurth, Alan. A Reference Architecture for Web Browsers (pdf)
- Gupta, Vineet. How Browsers Work - Part 1 - Architecture
-
解析
- Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (aka the "Dragon book"), Addison-Wesley, 1986
- Rick Jelliffe. The Bold and the Beautiful: two new drafts for HTML 5.
-
Firefox
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers.
- L. David Baron, Faster HTML and CSS: Layout Engine Internals for Web Developers (Google tech talk video)
- L. David Baron, Mozilla's Layout Engine
- L. David Baron, Mozilla Style System Documentation
- Chris Waterson, Notes on HTML Reflow
- Chris Waterson, Gecko Overview
- Alexander Larsson, The life of an HTML HTTP request
-
Webkit
- David Hyatt, Implementing CSS(part 1)
- David Hyatt, An Overview of WebCore
- David Hyatt, WebCore Rendering
- David Hyatt, The FOUC Problem
-
W3C 标准规范
-
浏览器构建说明
- Firefox. developer.mozilla.org/Build_Docum…
- WebKit. webkit.org/building/bu…