How Browsers Work: Behind the scenes of modern web browsers
难度系数:很难!没时间不要看。
By Tali Garsiel and Paul Irish
Published: August 5th, 2011
Comments: 38
本文是Paul Irish摘抄的Tali Garsiel的原文,但有一些不同(估计是他加上了自己的见解,也有一些错误),因此在看的时候,最好直接看原文!taligarsiel.com/
Preface
这篇关于 WebKit 和 Gecko 内部操作的综合入门文章是以色列开发人员 Tali Garsiel 进行大量研究的结果。几年来,她查看了所有已发布的有关浏览器内部结构的数据(请参阅参考资料),并花费大量时间阅读 Web 浏览器源代码。她写道:
在 IE 占据 90% (市场份额)的统治年代,除了把浏览器当成一个“黑匣子”,别无他法,但现在,开源浏览器占据了一半以上的使用份额,是窥探原理,看看网络浏览器里面有什么的好机会了。好吧,里面有数百万行 C++ 代码......
Tali 在她的网站 上发表了她的研究,但我们知道它值得更多的受众,因此我们将其整理并在此处重新发布。作为一名 Web 开发人员,了解浏览器操作的内部结构有助于您做出更好的决策并了解开发最佳实践背后的理由 。虽然这是一个相当长的文档,但我们建议您花一些时间深入研究;我们保证你会很高兴你这样做了。 Paul Irish,Chrome Developer Relations
Introduction
网络浏览器是使用最广泛的软件。在本入门教程中,我将解释它们如何在幕后工作。我们将看到当您在地址栏中输入时google.com
直到您在浏览器屏幕上看到 Google 页面的这段时间会发生什么。
The browsers we will talk about
目前桌面上使用的主要浏览器有五种: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%。在移动设备上,Android 浏览器、iPhone 和 Chrome 约占使用量的 54%。
The browser's main functionality
浏览器的主要功能是(通过从服务器请求并在浏览器窗口中显示)来呈现您选择的 Web 资源。资源通常是 HTML 文档,但也可能是 PDF、图像或其他类型的内容。资源的位置由用户使用 URI(统一资源标识符)指定。
浏览器解释和显示 HTML 文件的方式在 HTML 和 CSS 规范中指定。这些规范由W3C(万维网联盟)组织维护,该组织是 Web 的标准组织。多年来,浏览器仅符合部分规范并开发了自己的扩展。这给web作者带来了严重的兼容性问题。今天,大多数浏览器或多或少都符合规范。
浏览器用户界面彼此之间有很多共同点。常见的用户界面元素包括:
- 用于插入 URI 的地址栏
- 后退和前进按钮
- 书签选项
- 刷新和停止按钮,用于刷新或停止加载当前文档
- 将您带到主页的主页按钮
奇怪的是,浏览器的用户界面并没有在任何正式规范中指定,它只是来自多年经验和浏览器相互模仿形成的良好实践。HTML5 规范没有定义浏览器必须具有的 UI 元素,但列出了一些常见元素。其中包括地址栏、状态栏和工具栏。当然,Firefox 的下载管理器等特定浏览器具有独特的功能。
The browser's high level structure
浏览器的主要组件是(1.1):
- 用户界面 :这包括地址栏、后退/前进按钮、书签菜单等。除了您看到请求页面的主窗口之外,浏览器显示的其他部分。
- 浏览器引擎 :查询和操作渲染引擎的接口。
- 渲染引擎 :负责显示请求的内容。例如,如果请求的内容是 HTML,则渲染引擎会解析 HTML 和 CSS,并将解析后的内容显示在屏幕上。
- Networking :用于网络调用,如 HTTP 请求。它具有独立于平台的接口和对每个平台的底层实现。
- UI 后端 :用于绘制基本小部件,如组合框和窗口。它暴露了一个非特定平台的通用接口。在它下面使用操作系统的接口方法。
- JavaScript 解释器 。用于解析和执行 JavaScript 代码。
- 数据存储 。这是一个持久层。浏览器可能需要在本地保存各种数据,例如 cookie。新的HTML规范(HTML5)定义了'web database'是在浏览器中的一个完整(虽然轻量)的数据库。
Figure 1: Browser main components
需要注意的是,和其他浏览器不同,Chrome 会运行多个渲染引擎实例:每个选项卡一个。每个选项卡是一个独立的进程。
Communication between the components
Firefox 和 Chrome 都有一个特殊的通信基础设施,这将在一个特定的章节来讨论。
The rendering engine
渲染引擎的职责是... 渲染,即在浏览器屏幕上显示请求的内容。
默认情况下,渲染引擎可以显示 HTML 和 XML 文档和图像。可以通过插件(浏览器扩展)显示其他类型的数据;例如,使用 PDF 查看器插件显示 PDF 文档。插件和扩展将在特定的章节讨论。但是,在本章中,我们将重点关注主要用例:显示使用 CSS 格式化的 HTML 和图像。
Rendering engines
我们的参考浏览器:Firefox,Chrome和Safari都建立在两个渲染引擎上。Firefox使用Gecko——Mozilla自制渲染引擎。Safari和Chrome都使用Webkit。
WebKit 是一个开源渲染引擎,最初是作为 Linux 平台的引擎,后来被 Apple 修改以支持 Mac 和 Windows。有关详细信息,请参阅 webkit.org。
The main flow
渲染引擎将开始从网络层获取所请求文档的内容。这通常以 8kB 的块完成。
之后,这是渲染引擎的基本流程:
Figure 2: Rendering engine basic flow
渲染引擎将开始解析 HTML 文档并将 tag 转换为名为“content tree”的树中的DOM节点。它将解析外部 CSS 文件和 style 元素中的样式数据。这些样式信息和HTML中的视觉指令将用于创建另一棵树:render tree。
渲染树包含具有颜色和尺寸等视觉 attributes 的矩形。矩形按正确的顺序显示在屏幕上。
在构建渲染树之后,它会经历一个“layout”过程。这意味着为每个节点提供它应该出现在屏幕上的确切坐标。下一阶段是painting——(渲染引擎?)将遍历渲染树,并使用 UI 后端层绘制每个节点。
重要的是要理解这是一个渐进的过程。为了更好的用户体验,渲染引擎会尽量在屏幕上尽快显示内容。在开始构建和布局渲染树前,它不会等到所有 HTML 都被解析。渲染引擎进程先解析和显示部分内容,同时继续处理来自网络的源源不断的其余内容。
Main flow examples
Figure 3: WebKit main flow
Figure 4: Mozilla's Gecko rendering engine main flow (3.6)
从图 3 和图 4 可以看出,虽然 WebKit 和 Gecko 使用的术语略有不同,但流程基本相同。
Gecko 将视觉格式化元素的树称为“Frame Tree”。每个元素都是一个frame
。WebKit 使用术语“Render Tree”,它由“Render Objects”组成。WebKit 使用术语“Layout”来放置元素,而 Gecko 将其称为“Reflow”。“Attachment”是 WebKit 的术语,用于连接 DOM 节点和视觉信息以创建渲染树。一个小的非语义差异是 Gecko 在 HTML 和 DOM 树之间有一个额外的层。它被称为“content sink”,是制作 DOM 元素的工厂。我们将讨论流程的每个部分:
Parsing–general
由于解析在渲染引擎中是一个非常重要的过程,我们将研究的更深入一点。从对解析的简要介绍开始。
解析文档意味着将其转换为代码可以理解使用的有意义的结构。解析的结果通常是代表文档结构的节点树。这称为解析树或语法树。
例如:解析表达式 2 + 3 - 1
可以返回这棵树:
Figure 5: mathematical expression tree node
Grammars
解析基于文档遵循的语法规则:写作文档的语言或格式。您可以解析的每种格式都必须具有由词汇和语法规则组成的确定性语法。它被称为 context free grammar。人类语言不是这样的语言,因此不能用传统的解析技术来解析。
Parser–Lexer combination
解析可以分为两个子过程:词法分析和句法分析。
词法分析是将输入分解为tokens
的过程。Tokens
是语言词汇:有效构建块的集合。在人类语言中,它将由该语言词典中出现的所有单词组成。
token翻译为标记,但tag也可以,容易混淆,所以不翻译。英文原意为:a sequence of bits passed continuously between nodes in a fixed order and enabling a node to transmit information.
语法分析是语言语法规则的应用。
解析器通常将工作分为两个组件:负责将输入分解为有效tokens
的lexer (有时称为tokenizer),以及根据语言语法规则分析文档结构来负责构建解析树的parser。 lexer
知道如何去除不相关的字符,如空格和换行符。
Figure 6: from source document to parse trees
有很多英文术语,如 lexer, parser,还是不翻译的好,一是简洁醒目,二则可以与英文图对应上。下同。
解析过程是迭代的。parser
通常会向lexer
询问新的token
并尝试将token
与语法规则之一匹配。如果规则匹配,则将与该token
对应的节点添加到解析树中,然后parser
将请求另一个标记。
如果没有规则匹配,parser
将存储token
,并继续请求token
,直到找到匹配所有内部存储的token
的规则。如果没有找到规则,则parser
将报异常。这意味着该文档无效并且包含语法错误。
Translation
很多时候解析树不是最终产品。解析常用于翻译:将输入文档转换为另一种格式。一个例子是编译。将源代码编译为机器代码的compiler
首先将其解析为解析树,然后将此树翻译为机器代码文档。
Figure 7: compilation flow
Parsing example
在图 5 中,我们根据数学表达式构建了一个解析树。让我们尝试定义一个简单的数学语言,看看解析过程。
词汇:我们的语言可以包括整数、加号和减号。
句法:
- 语言语法构建块是表达式、术语和操作。
- 我们的语言可以包含任意数量的表达式。
- 表达式被定义为一个“术语”,后跟一个“操作”,然后是另一个术语
- 操作是加号或减号
- 术语是整数
token
或表达式
让我们分析输入2 + 3 - 1
.
第一个匹配规则的子字符串是2: 根据规则#5,它是一个术语。第二个匹配是2 + 3:这符合第三条规则:一个术语后跟一个操作,然后是另一个术语。下一个匹配只会在输入结束时触发。 2 + 3 - 1是一个表达式,因为我们已经知道2 + 3是一个术语,所以我们有一个术语,然后是一个操作,然后是另一个术语。 2 + +将不匹配任何规则,因此是无效输入。
Formal definitions for vocabulary and syntax
词汇通常用正则表达式表示。
例如我们的语言将被定义为:
INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -
可见,整数是由正则表达式定义的。
语法通常以称为BNF的格式定义。我们的语言将被定义为:
expression := term operation term
operation := PLUS | MINUS
term := INTEGER | expression
我们说过,如果一种语言的文法是上下文无关文法,那么它可以被常规解析器解析。上下文无关文法的直观定义是可以完全用 BNF 表达的文法。有关正式定义,请参阅 Wikipedia 关于上下文无关语法的文章
Types of parsers
解析器有两种类型:自顶向下解析器和自底向上解析器。一个直观的解释是自上而下的解析器检查语法的高级结构并尝试匹配其中之一。自底向上解析器从输入开始,逐渐将其转换为语法规则,从低级规则开始,直到满足高级规则。
让我们看看这两种类型的解析器将如何解析我们的示例。
自顶向下解析器将从更高级别的规则开始:它将识别2 + 3作为表达。然后它会识别2 + 3 - 1作为表达式(随着匹配其他规则,识别表达式的过程在演变,但起点是最高级别的规则)。
自下而上的解析器将扫描输入,直到匹配一个规则。然后它将用规则替换匹配的输入。这将一直持续到输入结束。部分匹配的表达式被放置在解析器的堆栈中。
Stack | Input |
---|---|
2 + 3 - 1 | |
term | + 3 - 1 |
term operation | 3 - 1 |
expression | - 1 |
expression operation | 1 |
expression |
这种自底向上的解析器称为移动减少解析器,因为输入是向右移动的(想象一个指针首先指向输入最左边并向右移动)并逐渐减少为语法规则。
有些单词如reduce在这里被软件直接翻成了归约,但在不理解归约含义的情况下,还不如直白的翻译为减少,更容易理解。
Generating parsers automatically
有些工具可以为你生成解析器——解析器生成器。你告诉他们你语言的语法——词汇和语法规则——然后他们生成一个有效的解析器。创建解析器需要对解析有深入的了解,手动创建一个优化的解析器并不容易,因此解析器生成器非常有用。
WebKit 使用两个众所周知的解析器生成器:用于创建lexer
的Flex和用于创建parser
的Bison(您可能会遇到它们的名称为 Lex
和 Yacc
)。Flex
的输入是一个包含tokens
的正则表达式定义的文件。Bison
的输入是 BNF
格式的语言语法规则。
HTML Parser
HTML 解析器的工作是将 HTML markup
解析为解析树。
markup: a set of tags assigned to elements of a text to indicate their relation to the rest of the text or dictate how they should be displayed.
The HTML grammar definition
HTML 的词汇和语法在 W3C 组织创建的 规范中定义。
Not a context free grammar
正如我们在解析介绍中看到的,grammar syntax
可以使用 BNF 等格式正式地定义。
不幸的是,所有传统的解析器主题都不适用于 HTML(我提出它们并不是为了好玩——它们将用于解析 CSS 和 JavaScript)。HTML 不能轻易地由解析器需要的上下文无关文法定义。
有一种用于定义 HTML 的正式格式——DTD(文档类型定义)——但它不是上下文无关的语法。
乍一看,这似乎很奇怪;HTML 相当接近 XML。有很多可用的 XML 解析器。HTML 有一个 XML 变体——XHTML——那么最大的区别是什么?
不同之处在于 HTML 方法更“宽容”:它允许您省略某些标记(然后隐式添加),或者有时省略开始或结束标记,等等。总的来说,它是一种“软”语法,与 XML 的僵硬严格的语法相反。
很明显这个看似很小的细节造成了巨大差别。一方面,这是 HTML 如此受欢迎的主要原因:它可以原谅您的错误,并使网络作者的生活变得轻松。另一方面,它使编写正规(formal)的语法变得困难。总之,传统解析器无法轻松解析 HTML,因为它的语法不是上下文无关的,XML解析器也不行。
HTML DTD
HTML 定义采用 DTD 格式。此格式用于定义SGML系列的语言。该格式包含所有允许的元素、它们的attributes
和层次结构的定义。如之前所见,HTML DTD 没有形成上下文无关文法。
attribute, property 都可以翻译为属性,为明确区分,还是不翻译为好。
DTD 有一些变体。严格模式仅符合规范,但其他模式包含对过去浏览器使用的markup
的支持。目的是向后兼容旧内容。当前严格的 DTD 在这里: www.w3.org/TR/html4/st…
DOM
输出树(“parse tree”)是 DOM 元素和attribute
节点的树。DOM 是Document Object Model (文档对象模型)的缩写。它是 HTML 文档的对象表示,也是 HTML 元素与 JavaScript 等外部世界的接口。
此树的根是“Document”对象。
DOM 与markup
几乎是一对一的关系。例如:
<html>
<body>
<p>
Hello World
</p>
<div> <img src="example.png"/></div>
</body>
</html>
此标记将被转换为以下 DOM 树:
Figure 8: DOM tree of the example markup
与 HTML 一样,DOM 由 W3C 组织指定。请参阅www.w3.org/DOM/DOMTR。 它是操作文档的通用规范。一个特定的模块描述了 HTML 特定的元素。HTML 的定义可以在这里找到: www.w3.org/TR/2003/REC…。
当我说树包含 DOM 节点时,我的意思是此树是由实现了某一个 DOM 接口的元素构成。浏览器使用具体实现(该实现有其他在浏览器内部使用的attributes
)。
The parsing algorithm
如前所见,不能使用常规的自顶向下或自底向上解析器来解析 HTML。
原因是:
- 语言的宽容性。
- 浏览器具有传统的容错能力以支持众所周知的无效 HTML 案例这一事实。
- 解析过程是可重入(reentrant)的。通常在解析时,源码不会改变,但在 HTML 中,包含
document.write()
的<script>
可以添加额外的tokens
,因此解析过程实际上会修改输入。
由于无法使用常规解析技术,浏览器会创建自定义解析器来解析 HTML。
HTML5 规范详细描述了解析算法。该算法包括两个阶段:tokenization
和树构建。
tokenization
是词法分析,将输入解析为token
。HTML token
包括开始tags
、结束tags
、attribute
名称和attribute
值。
tokenizer
识别token
,将其提供给树构造函数,并使用下一个字符来识别下一个token
,依此类推,直到输入结束。
Figure 9: HTML parsing flow (taken from HTML5 spec)
The tokenization algorithm
该算法的输出是一个 HTML token
。该算法表示为状态机。每个状态使用输入流的一个或多个字符,并根据这些字符更新下一个状态。该决定受当前tokenization
状态和树构造状态的影响。这意味着相同的耗用字符将根据当前状态为正确的下一个状态产生不同的结果。该算法过于复杂,无法完整描述,所以让我们看一个简单的例子,帮助我们理解原理。
基本示例——tokening
以下 HTML:
<html>
<body>
Hello world
</body>
</html>
下面的过程需要结合图来理解
初始状态是“Data”。当遇到字符<
时,状态变为 “Tag open” 。(此时)耗用一个a-z
字符会创建一个“start tag token”,状态更改为 “Tag name” 。一直保持这种状态,直到>
字符被使用。每个字符都附加到新的token
名称。在本例中,创建的标记是一个html
token
。
当到达>
标签时,当前token
被发出并且状态变回 “Data” 。<body>
标签将通过相同的步骤进行处理。到目前为止,html
andbody
标签已经发出。我们现在回到状态 “Data” 。使用Hello world
的H
字符将导致创建和发出一个字符token
,这种情况一直持续到达到</body>
的<
为止。我们将为Hello world
的每个字符发出一个字符token
。
状态现在回到了 “Tag open” 。使用下一个输入/
将导致创建一个end tag token
并移动到状态 "Tag name" 。再次保持这种状态,直到我们到达>
。然后新的标签token
将被发出,我们回到状态 “Data” 。</html>
的输入将像前面这个情况一样处理。
Figure : Tokenizing the example input
Tree construction algorithm
创建解析器时,会创建 Document 对象。在树构建阶段,将修改根中包含 Document 的 DOM 树,并向其中添加元素。tokenizer
发出的每个节点都将由树构造函数处理。对于每个token
,规范定义了与其相关并将被创建的 DOM 元素。该元素除了被添加到 DOM 树中,也被添加到open
元素的堆栈中。此堆栈用于更正嵌套不匹配和未关闭的tags
。该算法也被描述为一个状态机。这些状态称为“插入模式”。
看看示例输入的树构造过程:
<html>
<body>
Hello world
</body>
</html>
下面过程也需要结合图来理解
树构建阶段的输入是来自tokenization
阶段的一系列tokens
。第一种模式是 “initial mode” 。接收“html”token
将导致移动到 “before HTML” 模式并在该模式下重新处理token
。这将创建HTMLHtmlElement 元素,并把该元素附加到根 Document 对象。
状态将更改为 “before head” 。然后接收到“body”token
。尽管我们没有“head”token
,但 HTMLHeadElement 将被隐式创建,并将被添加到树中。
我们现在转到 “in head” 模式,然后转到 “after head” 。bodytoken
被重新处理,一个 HTMLBodyElement 被创建和插入,模式被转移到 "in body" 。
现在接收到“Hello world”字符串的字符tokens
。第一个token
将导致创建和插入一个“文本”节点,然后其他字符将附加到该节点。
body end token
的接收将导致转移到 “after body” 模式。我们现在将收到 html end token
,它将使我们进入 “after after body” 模式。接收到file end token
将结束解析。
Figure 10: tree construction of example html
Actions when the parsing is finished
在这个阶段,浏览器会将文档标记为交互式并开始解析处于“延迟”模式的脚本:那些应该在文档解析后执行的脚本。然后将文档状态设置为“完成”,并触发“加载”事件。
Browsers' error tolerance
您永远不会在 HTML 页面上收到“无效语法”错误。浏览器修复无效内容并继续(解析)。
以这个 HTML 为例:
<html>
<mytag>
</mytag>
<div>
<p>
</div>
Really lousy HTML
</p>
</html>
我一定违反了大约一百万条规则(“mytag”不是标准标签,“p”和“div”元素的错误嵌套等等)但浏览器仍然正确显示它并且没有抱怨。所以很多parse
代码都在修复 HTML author
的错误。
author 是指网页编写者,不能算为开发者(developer)
错误处理在浏览器中是相当一致的,但令人惊讶的是,它并没有成为 HTML 规范的一部分。就像书签和后退/前进按钮一样,它只是多年来在浏览器中开发的东西。在许多站点上重复存在已知的无效 HTML 结构,并且浏览器尝试以与其他浏览器一致的方式修复它们。
HTML5 规范确实定义了其中一些要求。(WebKit 在 HTML parser class的开头的注释中很好地总结了这一点。)
解析器将
tokenized
的输入解析到文档中,构建文档树。如果文档格式正确,则解析它很简单。
不幸的是,我们必须处理许多格式不正确的 HTML 文档,因此解析器必须能够容错。
我们必须至少注意以下错误情况:
- 在某些外部
tag
内明确禁止添加的元素。在这种情况下,我们应该关闭所有tags
,直到禁止元素的tag
,然后添加这个元素。- 我们不允许直接添加元素。可能是编写文档的人忘记了中间的某个
tag
(或者中间的tag
是可选的)。这可能是以下tag
的情况:HTML HEAD BODY TBODY TR TD LI(我忘了什么吗?)。- 我们想在内联元素中添加一个块元素。关闭所有内联元素,直到下一个更高的块元素。
- 如果这没有帮助,请关闭元素,直到我们被允许添加元素或忽略
tag
。
让我们看一些 WebKit 容错示例:
</br>
instead of <br>
有些网站使用 </br>
而不是 <br>
。为了兼容 IE 和 Firefox,WebKit 将其视为 <br>
。
代码:
if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
reportError(MalformedBRError);
t->beginTag = true;
}
请注意,错误处理是内部的:它不会呈现给用户。
A stray 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>
The code:
if (m_inStrayTableContent && localName == tableTag)
popBlock(tableTag);
WebKit 使用堆栈来存储当前元素的内容:它将内部表从外部表堆栈中弹出。这些表现在是同级的了。
Nested form elements
如果用户将一个表单放入另一个表单,则忽略第二个表单。 代码:
if (!m_currentFormElement) {
m_currentFormElement = new HTMLFormElement(formTag, m_document);
}
A too deep tag hierarchy
评论不言自明。
www.liceo.edu.mx 是一个站点的示例,它实现了大约 1500 个
tags
的嵌套级别(都来自一堆<b>
)。在忽略它们之前,我们最多只允许 20 个同类的tag
标签。
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;
}
Misplaced html or body end tags
再次——评论不言自明。
支持真正损坏的 HTML。我们从不关闭body tag,因为一些愚蠢的网页会在文档真正结束之前关闭它。让我们依靠 end() 调用来关闭东西。
if (t->tagName == htmlTag || t->tagName == bodyTag )
return;
所以web作者要小心——除非你想在 WebKit 容错代码片段中作为示例出现——编写格式良好的 HTML。
CSS parsing
还记得(parser)简介中的解析概念吗?嗯,与 HTML 不同,CSS 是一种上下文无关语法,可以使用(parser)简介中描述的解析器类型进行解析。事实上,CSS 规范定义了 CSS 词法和语法。
让我们看一些例子:
lexical grammar
(词汇)由每个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(由 "#" 引用)
lexical grammar
与syntax grammar
翻译出来会比较别扭,还是不翻为好。
syntax grammar
在 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 代表空白)。规则集包含花括号,其中包含一个声明或可选的多个用分号分隔的声明。“declaration”和“selector”将在随后的BNF定义中被定义。
WebKit CSS parser
WebKit 使用Flex 和 Bison解析器生成器从 CSS 语法文件自动创建解析器。正如您在解析器介绍中所记得的那样,Bison 创建了一个自下而上的 shift reduce
解析器。Firefox 使用手动编写的自顶向下解析器。在这两种情况下,每个 CSS 文件都被解析为一个 StyleSheet 对象。每个对象都包含 CSS 规则。CSS 规则对象包含选择器和声明对象以及与 CSS 语法对应的其他对象。
Figure 11: parsing CSS
Parsing scripts
这个将在JavaScript
的章节里处理。
The order of processing scripts and style sheets
Scripts
web模型是同步的。作者希望在解析器到达 <script>
tag
时立即解析和执行脚本。文档的解析会暂停,直到脚本执行完毕。如果脚本是外部的,那么必须从网络中获取资源(这也是同步完成的),解析会暂停,直到资源被获取。这是多年来的模型,并且在 HTML4 和 5 规范中也有规定。作者可以将“defer”属性添加到脚本中,在这种情况下,它不会停止文档解析,而是在文档解析后执行。HTML5 添加了一个将脚本标记为异步的选项,因此它将由不同的线程解析和执行。
Speculative parsing
WebKit 和 Firefox 都进行了这种优化。在执行脚本时,另一个线程解析文档的其余部分并找出需要从网络加载的其他资源并加载它们。通过这种方式,可以在并行连接上加载资源并提高整体速度。注意:推测parser
只解析对外部资源的引用,如外部脚本、样式表和图像:它不会修改 DOM 树——这事儿留给主parser
。
Style sheets
另一方面,样式表有不同的模型。从概念上看,由于样式表不会更改 DOM 树,因此没有理由等待它们并停止文档解析。但在文档解析阶段要求样式信息的脚本会存在问题。如果样式还没有加载和解析,脚本会得到错误的答案,显然这会导致很多问题。这似乎是一个边缘情况,但很常见。当样式表仍在加载和解析时,Firefox 会阻止所有脚本。WebKit 仅在脚本尝试访问可能受未加载样式表影响的某些样式properties
时才阻止脚本。
Render tree construction
在构建 DOM 树时,浏览器会构建另一棵树,即render tree
。该树是按将被显示的顺序排列的视觉元素。它是文档的可视化表示形式。此树的目的是能够以正确的顺序绘制内容。
Firefox 将render tree
中的元素称为“frames”。WebKit 使用术语renderer
或render object
。
renderer
知道如何布局和绘制自己及其子项。
WebKit 的 RenderObject 类,renderer
的基类,具有以下定义:
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
}
如 CSS2 规范所述,每个renderer
代表一个通常对应于节点的 CSS 盒子的矩形区域。它包括如宽度、高度和位置的几何信息。
盒子类型受与节点相关的样式attribute
的“display”值影响(请参阅style computation部分)。下为根据 display attribute
决定应该为 DOM 节点创建哪种类型的renderer
的 WebKit 代码:
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;
}
元素类型也被考虑在内:例如,表单控件和表格具有特殊的frames
。
在 WebKit 中,如果一个元素想要创建一个特殊的renderer
,它将覆盖createRenderer()
方法。renderer
指向包含非几何信息的样式对象。
The render tree relation to the DOM tree
renderer
对应 DOM 元素,但关系不是一对一的。不可见的 DOM 元素不会被插入到render tree
中。一个例子是“head”元素。display
属性值为“none”的元素也不会出现在树中(而具有“hidden”可见性属性的元素将出现在树中)。
有些 DOM 元素是对应于几个视觉对象的。通常是结构复杂的元素,不能用单个矩形来描述。例如,“select”元素有三个renderer
:一个用于显示区域,一个用于下拉列表框,一个用于按钮。此外,当由于一行的宽度不足而将文本分成多行时,新行将作为额外的renderer
添加。
多个renderer
的另一个示例是损坏的 HTML。根据 CSS 规范,内联元素必须仅包含块元素或仅包含内联元素。在混合内容的情况下,将创建匿名块renderer
来包裹内联元素。
一些render objects
对应一个 DOM 节点,但不在树中的同一位置。浮动和绝对定位的元素在文档流之外,放置在树的不同部分,并映射到真实frame
。占位符frame
是他们应该在的地方。
Figure 12: The render tree and the corresponding DOM tree (3.1). The "Viewport" is the initial containing block. In WebKit it will be the "RenderView" object
The flow of constructing the tree
这里的presentation应该是指展示层,没看标准,先只能猜了
在 Firefox 中,presentation
被注册为 DOM 更新的侦听器。presentation
将frame
的创建委托给FrameConstructor
,然后此构造函数解析样式(请参阅style computation)并创建一个frame
(见图4)。
在 WebKit 中,解析样式和创建renderer
的过程称为“attachment”(见图3)。每个 DOM 节点都有一个“attach”方法。attachment
是同步的,节点插入到 DOM 树,(此动作会)调用新节点的“attach”方法。
处理 html 和 body 标签会构造render tree root
。root render
对象对应于 CSS 规范中的containing block
:包含所有其他块的最顶层块。它的尺寸是视口:浏览器窗口显示区域的尺寸。Firefox 称它为ViewPortFrame
,WebKit 称它为RenderView
。这是document
指向的render
对象。树的其余部分被构造为 DOM 节点插入。
请参阅有关处理模型的 CSS2 规范。
Style Computation
构建render tree
需要计算每个render
对象的视觉properties
。这是通过计算每个元素的 style properties
来完成的。
样式包括各种来源的样式表、内联样式元素和 HTML 中的可视properties
(如“bgcolor” property
)。后者被翻译为匹配的 CSS style properties
。
样式表的起源是浏览器的默认样式表、页面作者提供的样式表和用户样式表——这些是浏览器用户提供的样式表(浏览器让您定义自己喜欢的样式。例如,在 Firefox 中,通过在“Firefox Profile”文件夹中放置一个样式表来完成)。
style 计算带来了一些困难:
-
style 数据是一个非常大的结构,包含许多样式
properties
,这可能会导致内存问题。 -
如未优化,则为每个元素查找匹配的规则可能会导致性能问题。为每个元素去遍历整个规则列表以查找匹配项是一项繁重的任务。选择器可能具有复杂的结构,这可能导致匹配过程从一条看起来有希望的路径开始,但却被证明是徒劳的,然后不得不尝试另一条路径。 例如——这个复合选择器:
div div div div{ ... }
意味着规则适用于是 3 个 div 的后代的
<div>
。假设您要检查规则是否适用于给定<div>
元素。您选择树上的特定路径进行检查。您可能需要向上遍历节点树,只是为了发现只有两个 div ,该规则不适用。然后,您需要尝试树中的其他路径。 -
应用这些规则涉及相当复杂的级联规则(定义规则层级的规则)。
让我们看看浏览器是如何面对这些问题的:
Sharing style data
WebKit 节点引用样式对象 (RenderStyle)。在某些情况下,这些对象可以由节点共享。这些节点是兄弟姐妹或堂兄弟,并且:
- 元素必须处于相同的鼠标状态(例如,不能一个处于 :hover 而另一个不是)
- 元素都不应该有 id
tag
名称应匹配class attributes
应该匹配The set of mapped attributes
必须相同links
状态必须匹配focus
状态必须匹配- 元素都不应受到
attribute
选择器的影响,其中“受影响”是指在选择器内的任何地方,都不能有任何使用了attribute
选择器的选择器匹配。 - 元素上不能有内联样式属性
- 绝不能使用兄弟选择器。当遇到任何兄弟选择器时,WebCore 会简单地抛出一个全局开关,并在它们存在时禁用整个文档的样式共享。这包括
+
选择器和:first-child
和:last-child
等选择器。
Firefox rule tree
Firefox 有两个额外的树来简化样式计算:rule tree
和style context tree
。WebKit 也有样式对象,但它们不存储在像style context tree
那样的树中,只是 DOM 节点指向其相关样式。
Figure 13: Firefox style context tree(2.2)
style context
包含(计算后的)最终值。通过以正确的顺序应用所有匹配规则并执行将它们从逻辑值转换为具体值的操作来计算这些值。例如,如果逻辑值是屏幕的百分比,它将被计算并转换为绝对单位。
rule tree
的想法非常聪明。它允许在节点之间共享这些值以避免再次计算它们。这也节省了空间。
所有匹配的规则都存储在树中。路径中的底部节点具有更高的优先级。树包含规则匹配到的所有路径。规则是惰性存储的。树不是在一开始对每个节点都去计算,但是每当需要计算节点样式时,计算的路径就会被添加到树中。
这个想法是将树路径视为词典中的单词。假设我们已经计算了这个rule tree
:
假设我们需要为context tree
中的另一个元素匹配规则,并找出匹配的规则(以正确的顺序)是 B-E-I。我们已经在树中有这条路径,因为我们已经计算了路径 A-B-E-I-L。我们现在要做的工作更少了。
让我们看看这棵树如何拯救我们的工作。
Division into structs
style context
分为许多结构。这些结构包含特定类别的样式信息,例如边框或颜色。结构中的所有属性要么是继承的,要么是非继承的。继承属性如果未定义就从其父元素继承属性。非继承属性(称为“重置”属性)如果未定义就使用默认值。
树通过在树中缓存整个结构(包含计算的最终值)来帮助我们。这个想法是,如果底部节点没有为结构提供定义,则可以使用上部节点中的缓存结构。
Computing the style contexts using the rule tree
在计算某个元素的样式上下文时,我们首先计算规则树中的路径或使用现有路径。然后我们开始在路径中应用规则来填充新样式上下文中的结构。我们从路径的底部节点开始——具有最高优先级的节点(通常是最具体的选择器)并向上遍历树直到我们的结构已满。如果该规则节点中的结构没有指定,那么我们可以大大优化——我们沿着树向上直到找到一个完全指定它的节点并简单地指向它——这是最好的优化——整个结构是共享的。这节省了最终值和内存的计算。如果我们找到部分定义,我们会沿着树向上直到结构被填充。
如果我们没有为我们的结构找到任何定义,那么如果该结构是“继承”类型,我们将指向上下文树 中父级的结构。在这种情况下,我们还成功地共享了结构。如果它是一个重置结构,那么将使用默认值。
如果最具体的节点确实添加了值,那么我们需要进行一些额外的计算以将其转换为实际值。然后我们将结果缓存在树节点中,以便子节点可以使用它。
如果一个元素有一个兄弟或兄弟指向同一个树节点,那么整个样式上下文 可以在它们之间共享。
让我们看一个例子:假设我们有这个 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>
和下面的规则:
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 结构包含四个边。 生成的规则树将如下所示(节点标有节点名称:它们指向的规则编号):
Figure : The rule tree
上下文树将如下(节点名称:它们指向的规则节点):
Figure : The context tree
Suppose we parse the HTML and get to the second
We now need to fill the style structs. We will begin by filling out the margin struct. Since the last rule node (F) doesn't add to the margin struct, we can go up the tree until we find a cached struct computed in a previous node insertion and use it. We will find it on node B, which is the uppermost node that specified margin rules.
We do have a definition for the color struct, so we can't use a cached struct. Since color has one attribute we don't need to go up the tree to fill other attributes. We will compute the end value (convert string to RGB etc) and cache the computed struct on this node.
The work on the second element is even easier. We will match the rules and come to the conclusion that it points to rule G, like the previous span. Since we have siblings that point to the same node, we can share the entire style context and just point to the context of the previous span.
For structs that contain rules that are inherited from the parent, caching is done on the context tree (the color property is actually inherited, but Firefox treats it as reset and caches it on the rule tree). For instance if we added rules for fonts in a paragraph:
p {font-family: Verdana; font size: 10px; font-weight: bold}
Then the paragraph element, which is a child of the div in the context tree, could have shared the same font struct as his parent. This is if no font rules were specified for the paragraph.
In WebKit, who does not have a rule tree, the matched declarations are traversed four times. First non-important high priority properties are applied (properties that should be applied first because others depend on them, such as display), then high priority important, then normal priority non-important, then normal priority important rules. This means that properties that appear multiple times will be resolved according to the correct cascade order. The last wins.
So to summarize: sharing the style objects (entirely or some of the structs inside them) solves issues 1 and 3. The Firefox rule tree also helps in applying the properties in the correct order.
Manipulating the rules for an easy match
There are several sources for style rules:
- CSS rules, either in external style sheets or in style elements.
p {color: blue}
- Inline style attributes like
<p style="color: blue" />
- HTML visual attributes (which are mapped to relevant style rules)
<p bgcolor="blue" />
The last two are easily matched to the element since he owns the style attributes and HTML attributes can be mapped using the element as the key.
As noted previously in issue #2, the CSS rule matching can be trickier. To solve the difficulty, the rules are manipulated for easier access.
After parsing the style sheet, the rules are added to one of several hash maps, according to the selector. There are maps by id, by class name, by tag name and a general map for anything that doesn't fit into those categories. If the selector is an id, the rule will be added to the id map, if it's a class it will be added to the class map etc. This manipulation makes it much easier to match rules. There is no need to look in every declaration: we can extract the relevant rules for an element from the maps. This optimization eliminates 95+% of the rules, so that they need not even be considered during the matching process(4.1).
Let's see for example the following style rules:
p.error {color: red}
#messageDiv {height: 50px}
div {margin: 5px}
The first rule will be inserted into the class map. The second into the id map and the third into the tag map. For the following HTML fragment;
<p class="error">an error occurred </p>
<div id=" messageDiv">this is a message</div>
We will first try to find rules for the p element. The class map will contain an "error" key under which the rule for "p.error" is found. The div element will have relevant rules in the id map (the key is the id) and the tag map. So the only work left is finding out which of the rules that were extracted by the keys really match. For example if the rule for the div was
table div {margin: 5px}
it will still be extracted from the tag map, because the key is the rightmost selector, but it would not match our div element, who does not have a table ancestor.
Both WebKit and Firefox do this manipulation.
Applying the rules in the correct cascade order
样式对象具有对应于每个视觉 attribute 的属性(所有 CSS attribute,但更通用)。如果该属性未由任何匹配的规则定义,则某些属性可以由父元素样式对象继承。其他属性具有默认值。
当有多个定义时,问题就开始了——级联顺序来解决这个问题。
Style sheet cascade order
A declaration for a style property can appear in several style sheets, and several times inside a style sheet. This means the order of applying the rules is very important. This is called the "cascade" order. According to CSS2 spec, the cascade order is (from low to high):1. Browser declarations
- User normal declarations
- Author normal declarations
- Author important declarations
- User important declarations
The browser declarations are least important and the user overrides the author only if the declaration was marked as important. Declarations with the same order will be sorted by specificity and then the order they are specified. The HTML visual attributes are translated to matching CSS declarations . They are treated as author rules with low priority.
样式属性的声明可以出现在多个样式表中,并且在一个样式表中出现多次。这意味着应用规则的顺序非常重要。这称为“级联”顺序。根据 CSS2 规范,级联顺序是(从低到高):
- 浏览器声明
- 用户正常声明
- 作者正常声明
- 作者重要声明
- 用户重要声明
浏览器声明最不重要,只有当声明被标记为重要时,用户的才会覆盖作者的。具有相同顺序的声明将按特异性排序,然后是它们指定的顺序。HTML 视觉属性被转换为匹配的 CSS 声明。它们被视为具有低优先级的作者规则。
Specificity
选择器特异性由CSS2 规范定义如下:
- 如果它来自的声明是“样式”属性而不是带有选择器的规则,则计数为 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 */
Sorting the rules
规则匹配后,按照级联规则进行排序。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;
}
Gradual process
WebKit 使用一个标志来标记是否所有顶级样式表(包括@imports)都已加载。如果附加时样式未完全加载,则使用占位符并在文档中进行标记,一旦加载样式表,将重新计算它们。
Layout
当渲染器被创建并添加到树中时,它没有位置和大小。计算这些值称为布局或回流。
HTML 使用基于流的布局模型,这意味着大多数时候可以一次计算几何图形。“流中”后面的元素通常不会影响“流中”较早的元素的几何形状,因此布局可以从左到右,从上到下通过文档进行。也有例外:例如,HTML 表格可能需要不止一次遍历 ( 3.5 )。
坐标系是相对于根框架的。使用顶部和左侧坐标。
布局是一个递归过程。它从根渲染器开始,它对应于HTML 文档的<html>
元素。布局通过部分或全部帧层次递归地继续,为每个需要它的渲染器计算几何信息。
根渲染器的位置是 0,0,它的尺寸是视口——浏览器窗口的可见部分。所有渲染器都有一个“布局”或“回流”方法,每个渲染器调用其需要布局的子级的布局方法。
Dirty bit system
为避免对每个小变化都进行一次完整的布局,浏览器使用一个“dirty bit”系统。更改或添加的渲染器将其自身及其子级标记为“dirty”:需要布局。
有两个标记:“dirty”和“子级 are dirty”,这意味着虽然渲染器本身可能没问题,但它至少有一个需要布局的子级。
Global and incremental layout
布局可以在整个渲染树上触发——这是“全局”布局。这可能由于以下原因而发生:
- 影响所有渲染器的全局样式更改,例如字体大小更改。
- 由于屏幕被调整大小
布局可以是增量的,只有 dirty 渲染器会被布局(这可能会导致一些问题,比如需要额外的布局)。 当渲染器为 dirty 时(异步)触发增量布局。例如,当额外的内容来自网络并被添加到 DOM 树后,新的渲染器被附加到渲染树。
Figure : Incremental layout–only dirty renderers and their children are laid out (3.6)
Asynchronous and Synchronous layout
增量布局是异步完成的。Firefox 对进行增量布局的“重排命令”进行排队,并且调度程序会触发这些命令的批量执行。WebKit 也有一个执行增量布局的计时器——树被遍历并且“dirty”的渲染器被执行布局。 要求样式信息的脚本,例如“offsetHeight”可以同步触发增量布局。 全局布局通常会同步触发。 有时布局会在初始布局后作为回调触发,因为某些属性(如滚动位置)发生了变化。
Optimizations
当布局由“resize”或渲染器位置(而不是大小)的更改触发时,渲染大小将从缓存中获取而不会重新进行计算。 在某些情况下,仅修改子树,布局就不会从根重新开始。这可能发生在更改是本地的并且不影响其周围环境的情况下,例如插入到文本字段中的文本(否则每次击键都会触发从根开始的布局)。
The layout process
布局通常具有以下模式:
- 父渲染器确定自己的宽度。
- 父渲染器依次处理子渲染器,并且:
- 放置子渲染器(设置 x,y 坐标)。
- 如果有必要,调用子渲染器的布局(如果子渲染器是 dirty 的,或者这是全局布局,或出于其他某些原因),这会计算子渲染器的高度。
- 父渲染器根据子渲染器的累加高度以及边距和填充的高度来设置自身高度,此值也可供父渲染器的父渲染器使用。
- 将其 dirty 位设置为 false。
Firefox 使用“state”对象 (nsHTMLReflowState) 作为布局的参数(称为“reflow”),这其中包括了父渲染器的宽度。 Firefox 布局的输出为“metrics”对象 (nsHTMLReflowMetrics),其包含渲染器的计算高度。
Width calculation
渲染器的宽度是使用容器块的宽度、渲染器的样式“width”属性、边距和边框来计算的。 例如以下 div 的宽度:
<div style="width: 30%"/>
Would be calculated by WebKit as the following(class RenderBox method calcWidth):* The container width is the maximum of the containers availableWidth and 0. The availableWidth in this case is the contentWidth which is calculated as:
将由 WebKit 计算如下(类 RenderBox 的 方法 calcWidth):
-
容器宽度是容器 availableWidth 和 0 中的最大值。在这种情况下,availableWidth 是 contentWidth,计算如下:
clientWidth() - paddingLeft() - paddingRight()
clientWidth 和 clientHeight 表示对象的内部,不包括边框和滚动条。
-
元素宽度是“宽度”样式属性。它将通过计算容器宽度的百分比来计算为绝对值。
-
添加了水平边框和填充。
到目前为止,这是“首选宽度”的计算。现在将计算最小和最大宽度。 如果首选宽度大于最大宽度,则使用最大宽度。如果它小于最小宽度(最小的牢不可破的单位),则使用最小宽度。
这些值会被缓存,以用于需要布局但宽度不变的情况。
Line Breaking
当在布局中的渲染器需要换行时,它会停下来并告知父级它需要换行。父级创建额外的渲染器并对其布局。
Painting
在绘制阶段,渲染树会被遍历,渲染器的“paint()”方法会被调用,并在屏幕上显示内容。绘制使用 UI 基础结构组件。
Global and Incremental
像布局一样,绘制也可以是全局(整棵树被绘制)的或增量的。在增量绘制中,一些渲染器的变化不会影响整个树。更改后的渲染器使其在屏幕上的矩形无效。这会导致操作系统将其视为“dirty 区”并生成“paint”事件。操作系统巧妙地做到了这一点,并将几个区域合并为一个。在 Chrome 中它更复杂,因为渲染器与主进程处于不同的进程中。Chrome 在一定程度上模拟了操作系统的行为。展示层监听这些事件并将消息委托给渲染根。遍历渲染树,直到到达相关的渲染器。该渲染器会重新绘制自己(通常也包括它的子级)。
The painting order
CSS2 定义了绘制过程的顺序。这实际上是元素在堆叠上下文中的堆叠顺序。此顺序会影响绘制,因为堆栈是从后向前绘制的。块渲染器的堆叠顺序是:
- background color
- background image
- border
- children
- outline
Firefox display list
Firefox 遍历渲染树并为绘制的矩形构建一个显示列表。它以正确的绘制顺序(渲染器的背景,然后是边框等)包含了与矩形相关的渲染器。这样一来,树只需要遍历一次即可重新绘制,而不是多次(绘制所有背景,然后是所有图像,然后是所有边框等)。
Firefox 通过不添加将被隐藏的元素来优化该过程,例如完全在其他不透明元素之下的元素。
WebKit rectangle storage
在重新绘制之前,WebKit 会将原来的矩形另存为一张位图,然后只绘制新旧矩形之间的差异部分。
Dynamic changes
浏览器会尝试执行尽可能少的操作以响应更改。因此,对元素颜色的更改只会导致此元素重绘。对元素位置的更改将导致元素、其子元素和可能的兄弟元素的布局和重绘。添加 DOM 节点将导致节点的布局和重绘。重大更改,例如增加“html”元素的字体大小,将导致缓存失效、重新布局和重新绘制整个树。
The rendering engine's threads
渲染引擎是单线程的。除了网络操作之外,几乎所有事情都发生在一个线程中。在 Firefox 和 Safari 中,这是浏览器的主线程。在 Chrome 中,它是选项卡进程的主线程。 网络操作可以由多个并行线程执行。并行连接的数量是有限的(通常是 2-6 个连接)。
Event loop
浏览器主线程是一个事件循环。这是一个使进程处于激活状态的无限循环。它等待着事件(如布局和绘制事件)并处理它们。这是Firefox中主事件循环的代码:
while (!mExiting)
NS_ProcessNextEvent(thread);
CSS2 visual model
The canvas
根据CSS2 规范,术语画布描述为“渲染格式化结构的空间”:浏览器绘制内容的地方。对于空间的每个维度,画布都是无限的,但浏览器会根据视口的维度选择初始宽度。
根据www.w3.org/TR/CSS2/zin…,如果包含在另一个画布中,画布是透明的,如果不是,则由浏览器指定颜色。
CSS Box model
CSS 框模型描述了为文档树中的元素生成的矩形框,并根据视觉格式化模型进行布局 。 每个框都有一个内容区域(例如文本、图像等)和可选的周围填充、边框和边距区域。
Figure : CSS2 box model
每个节点生成 0..n 个这样的框。 所有元素都有一个“显示”属性,该属性确定将生成的框的类型。例子:
block: 生成块盒子.
inline: 生成一个或多个内联盒子.
none: 不生成盒子.
默认值是内联的,但浏览器样式表可能会设置其他默认值。例如:“div”元素的默认显示是块。 您可以在此处找到默认样式表示例:www.w3.org/TR/CSS2/sam…
Positioning scheme
有以下三种方案:
- 正常:对象根据其在文档中的位置定位。这意味着它在渲染树中的位置就像它在 DOM 树中的位置一样,并根据其框类型和尺寸进行布局
- 浮动:对象首先像正常流一样布局,然后尽可能向左或向右移动
- 绝对:对象被放置在渲染树中与 DOM 树中不同的位置
定位方案由“position”属性和“float”属性设置。
- static 和 relative 导致正常流动
- absolute 和 fixed 导致绝对定位
在 static 定位中,没有定义位置,而是使用默认定位。在其他方案中,作者指定位置:上、下、左、右。
盒子的布局方式由以下因素决定:
- 盒子类型
- 盒子尺寸
- 定位方案
- 图像大小和屏幕大小等外部信息
Box types
块框:形成一个块——在浏览器窗口中有自己的矩形。
Figure : Block box
内联框:没有自己的块,但在包含块内。
Figure : Inline boxes
块一个接一个地垂直格式化。内联是水平格式化的。
Figure : Block and Inline formatting
内联框放在行或“行框”内。这些线至少与最高的盒子一样高,但可以更高,当盒子对齐“基线”时 - 意味着元素的底部与底部以外的另一个盒子的一个点对齐。如果容器宽度不够,内联会放在几行上。这通常发生在段落中。
Figure: Lines
Positioning
Relative
相对定位——像往常一样定位,然后移动所需的增量。
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
不管正常流,布局是被精确定义的。该元素不参与正常流。尺寸是相对于包含容器的。在fixed中,容器就是视口。
Figure : Fixed positioning
注意:即使文档滚动,fixed 框也不会移动!
Layered representation
这是由 z-index CSS 属性指定的。它代表盒子的第三个维度:沿“z 轴”的位置。
这些盒子被分成堆栈(称为堆栈上下文)。在每个堆栈中,后面的元素将首先被绘制,前面的元素在顶部,更靠近用户。如果重叠,最前面的元素将隐藏前一个元素。 堆栈根据 z-index 属性进行排序。具有“z-index”属性的框形成一个本地堆栈。视口具有外部堆栈。
Example:
<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 在HTML标记中的位置比绿色 div 靠前(在常规流程中应该优先绘制),但是 z-index 属性的优先级更高,因此它移动到了根框所保持的堆栈中更靠前的位置。
Resources
- Browser architecture
- Grosskurth, Alan. A Reference Architecture for Web Browsers (pdf)
- Gupta, Vineet. How Browsers Work–Part 1–Architecture
- Parsing
- 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 Specifications
- Browsers build instructions
- Firefox. developer.mozilla.org/en/Build_Do…
- WebKit. webkit.org/building/bu…
Tali Garsiel is a developer in Israel. She started as a web developer in 2000, and became aquainted with Netscape's "evil" layer model. Just like Richard Feynmann, she had a fascination for figuring out how things work so she began digging into browser internals and documenting what she found. Tali also has published a short guide on client-side performance.