浏览器的工作原理

介绍

Web 浏览器可能是使用最广泛的软件。在这本书中,我将解释它们在幕后是如何工作的。我们会看到当您在地址栏中输入“google.com”时会发生什么,直到您在浏览器屏幕上看到 Google 页面。

我们将讨论的浏览器

当今使用的主要浏览器有五种 - Internet Explorer、Firefox、Safari、Chrome 和 Opera。
我将给出来自开源浏览器的示例 - Firefox、Chrome 和 Safari,它们是部分开源的。
W3C 浏览器统计,目前(2009 年 10 月),Firefox、Safari 和 Chrome 的使用份额合计接近 60%。
所以现在开源浏览器是浏览器业务的重要组成部分。

浏览器的主要功能

浏览器的主要功能是通过从服务器请求并将其显示在浏览器窗口中来呈现您选择的 Web 资源。资源格式通常是 HTML,但也有 PDF、图像等。资源的位置由用户使用 URI(统一资源标识符)指定。更多关于网络章节的内容。

浏览器解释和显示 HTML 文件的方式在 HTML 和 CSS 规范中指定。这些规范由W3C(万维网联盟)组织维护,该组织是 Web 的标准组织。
HTML 的当前版本是 4 ( http://www.w3.org/TR/html401/ )。第 5 版正在进行中。当前的 CSS 版本是 2 ( http://www.w3.org/TR/CSS2/ ),版本 3 正在进行中。
多年来,浏览器只符合规范的一部分并开发了自己的扩展。这给网络作者带来了严重的兼容性问题。今天,大多数浏览器或多或少都符合规范。

浏览器的用户界面彼此之间有很多共同之处。常见的用户界面元素包括:

  • 用于插入 URI 的地址栏
  • 后退和前进按钮
  • 书签选项
  • 刷新和停止按钮,用于刷新和停止加载当前文档
  • 主页按钮可让您进入主页
奇怪的是,浏览器的用户界面没有在任何正式规范中指定,它只是多年经验和浏览器相互模仿形成的良好实践。HTML5 规范没有定义浏览器必须具有的 UI 元素,但列出了一些常见元素。其中包括地址栏、状态栏和工具栏。当然,Firefox 下载管理器等特定浏览器具有独特的功能。
更多关于用户界面章节的内容。

 

浏览器的高层结构

浏览器的主要组件是(1.1):

  1. 用户界面 - 这包括地址栏、后退/前进按钮、书签菜单等。除了您看到请求页面的主窗口外,浏览器的每个部分都显示出来。
  2. 浏览器引擎——查询和操作渲染引擎的接口。
  3. 渲染引擎——负责显示请求的内容。例如,如果请求的内容是HTML,它负责解析HTML和CSS,并将解析后的内容显示在屏幕上。
  4. 网络 - 用于网络调用,如 HTTP 请求。它具有独立于平台的接口和每个平台的底层实现。
  5. UI 后端 - 用于绘制基本小部件,如组合框和窗口。它公开了一个非平台特定的通用接口。在它下面使用操作系统用户界面方法。
  6. JavaScript 解释器。用于解析和执行 JavaScript 代码。
  7. 数据存储。这是一个持久层。浏览器需要在硬盘上保存各种数据,例如cookies。新的 HTML 规范 (HTML5) 定义了“网络数据库”,它是浏览器中一个完整(虽然轻量级)的数据库。

 


图 1:浏览器主要组件。

需要注意的是,与大多数浏览器不同,Chrome 拥有渲染引擎的多个实例——每个选项卡一个。每个选项卡都是一个单独的进程。

我将用一章来介绍这些组件中的每一个。

组件之间的通信

Firefox 和 Chrome 都开发了一种特殊的通信基础设施。
它们将在专门的章节中讨论。

渲染引擎

渲染引擎的职责就是... 渲染,即在浏览器屏幕上显示所请求的内容。

默认情况下,渲染引擎可以显示 HTML 和 XML 文档和图像。它可以通过插件(浏览器扩展)显示其他类型。一个示例是使用 PDF 查看器插件显示 PDF。我们将在专门的章节中讨论插件和扩展。在本章中,我们将关注主要用例 - 显示使用 CSS 格式化的 HTML 和图像。

渲染引擎

我们的参考浏览器 - Firefox、Chrome 和 Safari 建立在两个渲染引擎之上。Firefox 使用 Gecko——一种“自制的”Mozilla 渲染引擎。Safari 和 Chrome 都使用 Webkit。

Webkit 是一个开源渲染引擎,最初是作为 Linux 平台的引擎,经过 Apple 修改以支持 Mac 和 Windows。有关更多详细信息,请参阅http://webkit.org/

主要流程

渲染引擎将开始从网络层获取请求文档的内容。这通常以 8K 块的形式完成。

之后这是渲染引擎的基本流程:


图 2:渲染引擎​​基本流程。

 

渲染引擎将开始解析 HTML 文档并将标签转换为称为“内容树”的树中的DOM节点。它将解析外部 CSS 文件和样式元素中的样式数据。样式信息和 HTML 中的视觉指令将用于创建另一棵树 -渲染树

渲染树包含具有视觉属性(如颜色和尺寸)的矩形。矩形按正确的顺序显示在屏幕上。

在构建渲染树之后,它会经历一个“布局”过程。这意味着为每个节点提供它应该出现在屏幕上的确切坐标。下一阶段是绘制- 将遍历渲染树并使用 UI 后端层绘制每个节点。

重要的是要了解这是一个渐进的过程。为了获得更好的用户体验,渲染引擎会尽快将内容显示在屏幕上。在开始构建和布局渲染树之前,它不会等到所有 HTML 都被解析。部分内容将被解析和显示,而该过程将继续处理来自网络的其余内容。

主要流程示例

 


图 3:Webkit 主流程


图 4:Mozilla 的 Gecko 渲染引擎主流程(3.6

从图 3 和图 4 可以看出,虽然 Webkit 和 Gecko 使用的术语略有不同,但流程基本相同。
Gecko 将视觉格式化元素树称为框架树。每个元素都是一个框架。Webkit 使用术语“渲染树”,它由“渲染对象”组成。Webkit 使用术语“布局”来放置元素,而 Gecko 将其称为“回流”。“附件”是 Webkit 的术语,用于连接 DOM 节点和视觉信息以创建渲染树。一个小的非语义差异是 Gecko 在 HTML 和 DOM 树之间有一个额外的层。它被称为“内容接收器”,是制作 DOM 元素的工厂。我们将讨论流程的每个部分:

 

解析 - 一般

由于解析是渲染引擎中一个非常重要的过程,我们将更深入地研究它。让我们从关于解析的一些介绍开始。

解析文档意味着将其转换为某种有意义的结构——代码可以理解和使用的结构。解析的结果通常是表示文档结构的节点树。它被称为解析树或语法树。

示例 - 解析表达式“2 + 3 - 1”可以返回这棵树:


图5:数学表达式树节点

语法

解析基于文档遵循的语法规则 - 编写它的语言或格式。您可以解析的每种格式都必须具有由词汇和语法规则组成的确定性语法。它被称为 上下文无关文法人类语言不是这样的语言,因此不能用传统的解析技术来解析。

解析器 - 词法分析器组合

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

词法分析是将输入分解为标记的过程。令牌是语言词汇——有效构建块的集合。在人类语言中,它将包含出现在该语言词典中的所有单词。

语法分析是语言语法规则的应用。

解析器通常在两个组件之间划分工作 -负责将输入分解为有效标记词法分析器(有时称为标记器),以及负责根据语言语法规则通过分析文档结构来构建解析树解析器词法分析器知道如何去除不相关的字符,如空格和换行符。


图 6:从源文档到解析树

 

解析过程是迭代的。解析器通常会向词法分析器询问新的标记,并尝试将标记与语法规则之一进行匹配。如果匹配规则,则对应于令牌的节点将添加到解析树中,解析器将请求另一个令牌。
如果没有规则匹配,解析器将在内部存储令牌,并不断询问令牌,直到找到与所有内部存储的令牌匹配的规则。如果未找到规则,则解析器将引发异常。这意味着该文档无效并且包含语法错误。

翻译

很多时候解析树不是最终产品。解析通常用于翻译 - 将输入文档转换为另一种格式。一个例子是编译。将源代码编译成机器码的编译器首先将其解析成解析树,然后将树翻译成机器代码文档。


图 7:编译流程

解析示例

在图 5 中,我们根据数学表达式构建了一个解析树。让我们试着定义一个简单的数学语言,看看解析过程。

 

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

句法:

  1. 语言语法构建块是表达式、术语和操作。
  2. 我们的语言可以包含任意数量的表达式。
  3. 表达式被定义为一个“术语”,然后是一个“操作”,然后是另一个术语
  4. 操作是加号还是减号
  5. 术语是整数标记或表达式

 

让我们分析输入“2 + 3 - 1”。
匹配规则的第一个子串是“2”,根据规则#5,它是一个术语。第二个匹配是“2 + 3”,它匹配第二个规则 - 一个术语后跟一个操作,然后是另一个术语。下一场比赛只会在输入结束时命中。“2 + 3 - 1”是一个表达式,因为我们已经知道 ?2+3? 是一个术语,所以我们有一个术语,然后是一个操作,然后是另一个术语。“2++”将不匹配任何规则,因此是无效输入。

词汇和语法的正式定义

词汇通常用正则表达式表示

例如,我们的语言将被定义为:

整数 :0|[1-9][0-9]*
加:+
减: -
如您所见,整数由正则表达式定义。

 

语法通常以称为BNF的格式定义我们的语言将被定义为:

表达式 := term 操作 term
操作 := PLUS | 
术语 := 整数 | 表达

 

我们说过,如果一种语言的语法是上下文无关语法,则它可以被常规解析器解析上下文无关文法的直观定义是可以完全用 BNF 表达的文法。有关正式定义,请参阅 http://en.wikipedia.org/wiki/Context-free_grammar

解析器的类型

解析器有两种基本类型——自顶向下解析器和自底向上解析器。一个直观的解释是自顶向下的解析器看到语法的高级结构并尝试匹配其中之一。自下而上的解析器从输入开始,逐渐将其转换为语法规则,从低级规则开始,直到满足高级规则。

让我们看看这两种类型的解析器将如何解析我们的示例:

自上而下的解析器将从更高级别的规则开始 - 它会将“2 + 3”识别为表达式。然后它将“2 + 3 - 1”识别为一个表达式(识别表达式的过程与其他规则匹配,但起点是最高级别的规则)。

自底向上解析器将扫描输入直到匹配规则,然后将匹配的输入替换为规则。这将一直持续到输入结束。部分匹配的表达式放置在解析器堆栈中。

输入
  2 + 3 - 1
学期 + 3 - 1
定期操作 3 - 1
表达 - 1
表达式操作 1
表达  

这种自底向上的解析器被称为移位归约解析器,因为输入被向右移位(想象一个指针首先指向输入开始并向右移动)并逐渐减少到语法规则。

自动生成解析器

有一些工具可以为您生成解析器。它们被称为解析器生成器。你用你的语言的语法——它的词汇和语法规则来喂养它们,然后它们生成一个工作解析器。创建解析器需要对解析有深入的了解,手动创建优化的解析器并不容易,因此解析器生成器非常有用。

Webkit使用两个众所周知的解析器生成器——Flex 用于创建词法分析器和 Bison 用于创建解析器(您可能会遇到它们的名称 Lex 和 Yacc)。Flex 输入是一个包含标记的正则表达式定义的文件。Bison 的输入是 BNF 格式的语言语法规则。

HTML解析器

HTML 解析器的工作是将 HTML 标记解析为解析树。

HTML 语法定义

HTML 的词汇和语法在 w3c 组织创建的规范中定义当前版本是 HTML4,HTML5 的工作正在进行中。

不是上下文无关语法

正如我们在解析介绍中看到的那样,可以使用 BNF 等格式正式定义文法句法。
不幸的是,所有传统的解析器主题都不适用于 HTML(我提出它们不是为了好玩——它们将用于解析 CSS 和 JavaScript)。HTML 不能被解析器需要的上下文无关语法轻易定义。
有一种定义 HTML 的正式格式——DTD(文档类型定义)——但它不是上下文无关的语法。
这在第一个站点看起来很奇怪 - HTML 与 XML 相当接近。有很多可用的 XML 解析器。HTML 有一个 XML 变体 - XHTML - 那么最大的区别是什么?
不同之处在于 HTML 方法更“宽容”,它允许您省略某些隐式添加的标签,有时会省略标签的开始或结束等。总的来说,它是一种“软”语法,与 XML 的僵硬和要求相反句法。
显然,这种看似微小的差异使世界变得不同。一方面,这是 HTML 如此流行的主要原因 - 它可以原谅您的错误并使网络作者的生活变得轻松。另一方面,它使得编写格式语法变得困难。总而言之 - HTML 无法轻松解析,传统解析器无法解析,因为其语法不是上下文无关语法,XML 解析器也无法解析。

HTML DTD

HTML 定义采用 DTD 格式。此格式用于定义SGML家族的语言该格式包含所有允许元素的定义、它们的属性和层次结构。正如我们之前看到的,HTML DTD 没有形成上下文无关文法。

DTD 有一些变体。严格模式仅符合规范,但其他模式包含对浏览器过去使用的标记的支持。目的是向后兼容旧内容。当前严格的 DTD 在这里:http : //www.w3.org/TR/html4/strict.dtd

DOM

输出树 - 解析树是 DOM 元素和属性节点的树。DOM 是文档对象模型的缩写。它是 HTML 文档的对象表示,是 HTML 元素与 JavaScript 等外部世界的接口。
树的根是“文档”对象。

DOM 与标记几乎是一对一的关系。例如,这个标记:

<html>
	<身体>
		<p>
			你好世界
		</p>
		<div> <img src="example.png"/></div>
	</正文>
</html>
将被转换为以下 DOM 树:

图 8:示例标记的 DOM 树

 

与 HTML 一样,DOM 由 w3c 组织指定。请参阅http://www.w3.org/DOM/DOMTR它是用于操作文档的通用规范。特定模块描述 HTML 特定元素。HTML 定义可以在这里找到:http : //www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/idl-definitions.html

当我说树包含 DOM 节点时,我的意思是树是由实现其中一个 DOM 接口的元素构成的。浏览器使用具有浏览器内部使用的其他属性的具体实现。

解析算法

正如我们在前几节中看到的,无法使用常规的自顶向下或自底向上解析器来解析 HTML。

原因是:

  1. 语言的宽容性。
  2. 事实上,浏览器具有传统的容错能力来支持众所周知的无效 HTML 案例。
  3. 可重入的解析过程。通常在解析过程中源不会改变,但在 HTML 中,包含“document.write”的脚本标签可以添加额外的标记,因此解析过程实际上会修改输入。

由于无法使用常规解析技术,浏览器会创建自定义解析器来解析 HTML。

解析算法在 HTML5 规范中有详细描述。该算法由两个阶段组成 - 标记化和树构建。

标记化是词法分析,将输入解析为标记。HTML 标记包括开始标记、结束标记、属性名称和属性值。

分词器识别标记,将其提供给树构造函数并使用下一个字符来识别下一个标记,依此类推,直到输入结束。


图 6:HTML 解析流程(取自 HTML5 规范)

 

标记化算法

该算法的输出是一个 HTML 标记。该算法表示为状态机。每个状态消耗输入流的一个或多个字符,并根据这些字符更新下一个状态。该决定受当前标记化状态和树构造状态的影响。这意味着相同的消耗字符将为正确的下一个状态产生不同的结果,具体取决于当前状态。该算法过于复杂,无法全面展开,所以让我们看一个简单的例子来帮助我们理解原理。

基本示例 - 标记以下 HTML:

<html>
	<身体>
		你好世界
	</正文>
</html>
初始状态是“数据状态”。当遇到“<”字符时,状态变为“标签打开状态”使用 "az" 字符会导致创建 "Start tag token",状态更改为"Tag name state"我们一直保持这种状态,直到 ">" 字符被消耗掉。每个字符都附加到新的令牌名称。在我们的例子中,创建的令牌是一个“html”令牌。
当到达 ">" 标记时,发出当前标记并且状态变回"Data state""<body>" 标签将按照相同的步骤进行处理。到目前为止,已发出“html”和“body”标签。消耗“Hello world”的“H”字符将导致创建和发出字符标记,一直持续到“</body>”的“<”到达。我们将为“Hello world”的每个字符发出一个字符标记。
我们现在回到“标签打开状态”使用下一个输入“/”将导致创建“结束标记标记”并移动到“标记名称状态”我们再次保持这个状态,直到我们到达 ">"。然后将发出新的标记令牌,我们返回到"Data state"“</html>”输入将像前一种情况一样处理。

图 9:标记示例输入

 

建树算法

创建解析器时,会创建 Document 对象。在树构建阶段,将修改根中包含 Document 的 DOM 树,并向其中添加元素。标记器发出的每个节点都将由树构造函数处理。对于每个令牌,规范定义了与其相关的 DOM 元素并将为此令牌创建。除了将元素添加到 DOM 树之外,它还被添加到一个开放元素堆栈中。此堆栈用于纠正嵌套不匹配和未关闭的标签。该算法也被描述为状态机。这些状态被称为“插入模式”。

让我们看看示例输入的树构建过程:

<html>
	<身体>
		你好世界
	</正文>
</html>

 

树构建阶段的输入是来自标记化阶段的一系列标记。第一种模式是“初始模式”接收 html 令牌将导致移动到“before html”模式并在该模式下重新处理令牌。这将导致创建 HTMLHtmlElement 元素,并将其附加到根 Document 对象。
状态将更改为"before head"我们收到“body”令牌。尽管我们没有“头”标记,但将隐式创建 HTMLHeadElement 并将其添加到树中。
我们现在进入“头部”模式,然后进入“头部之后”body 标记被重新处理,一个 HTMLBodyElement 被创建和插入,模式被转移到"in body"
现在接收“Hello world”字符串的字符标记。第一个将导致创建和插入“文本”节点,其他字符将附加到该节点。
身体结束令牌的接收将导致转移到“身体后”模式。我们现在将收到 html 结束标记,它将使我们进入“after after body”模式。接收到文件结束标记将结束解析。


图 10:示例 html 的树结构

 

解析完成时的动作

在这个阶段,浏览器会将文档标记为交互式并开始解析处于“延迟”模式的脚本 - 那些应该在解析文档后执行的脚本。然后将文档状态设置为“完成”并触发“加载”事件。

您可以在 HTML5 规范中查看标记化和树构建的完整算法 - http://www.w3.org/TR/html5/syntax.html#html-parser

浏览器容错

您永远不会在 HTML 页面上收到“无效语法”错误。浏览器修复无效内容并继续。
以这个 HTML 为例:

<html>
  <我的标签>
  </我的标签>
  <div>
  <p>
  </div>
  	非常糟糕的 HTML
  </p>
</html>
我一定违反了大约一百万条规则(“mytag”不是标准标签,“p”和“div”元素的错误嵌套等等)但浏览器仍然正确显示它并且没有抱怨。所以很多解析器代码都在修复 HTML 作者的错误。

 

错误处理在浏览器中非常一致,但令人惊讶的是它不是 HTML 当前规范的一部分。就像书签和后退/前进按钮一样,它只是多年来在浏览器中开发的东西。已知的无效 HTML 结构在许多站点中重复出现,浏览器尝试以与其他浏览器一致的方式修复它们。

HTML5 规范确实定义了其中一些要求。Webkit 在 HTML 解析器类开头的注释中很好地总结了这一点

 

解析器将标记化的输入解析到文档中,构建文档树。如果文档格式正确,解析它很简单。
 
不幸的是,我们必须处理许多格式不正确的 HTML 文档,因此解析器必须能够容忍错误。
 
我们必须至少处理以下错误情况:
 
1. 被添加的元素被明确禁止在一些外部标签内。
在这种情况下,我们应该关闭所有标签,直到禁止该元素的那个,然后再添加它。
 
2. 不允许直接添加元素。 
可能是编写文档的人忘记了中间的某个标签(或者中间的标签是可选的)。
以下标签可能就是这种情况:HTML HEAD BODY TBODY TR TD LI(我忘记了吗?)。
 
3. 我们想在内联元素中添加一个块元素。关闭所有行内元素直到下一个更高的块元素。
 
4. 如果这没有帮助,请关闭元素,直到我们被允许添加元素或忽略标签。

 

让我们看一些 Webkit 容错示例:

</br> 而不是 <br>

某些站点使用</br> 而不是<br>。为了兼容 IE 和 Firefox,Webkit 将此视为 <br>。
代码:

if (t->isCloseTag(brTag) && m_document->inCompatMode()) {
     报告错误(MalformedBRError);
     t->beginTag = 真;
}
注意 - 错误处理是内部的 - 它不会呈现给用户。

 

一张流浪表

杂散表格是位于另一个表格内容内但不在表格单元格内的表格。
像这个例子:

<表>
	<表>
		<tr><td>内表</td></tr>
         </table>
	<tr><td>外表</td></tr>
</table>
Webkit 会将层次结构更改为两个兄弟表:
<表>
	<tr><td>外表</td></tr>
</table>
<表>
	<tr><td>内表</td></tr>
 </table>
代码:
if (m_inStrayTableContent && localName == tableTag)
        popBlock(tableTag);
Webkit 为当前元素内容使用一个堆栈 - 它会将内部表从外部表堆栈中弹出。这些表现在将是兄弟姐妹。

 

嵌套表单元素

如果用户将一个表单放入另一个表单中,则第二个表单将被忽略。
代码:

如果(!m_currentFormElement){
        m_currentFormElement = new HTMLFormElement(formTag, m_document);
}

 

标签层次太深

评论不言自明。

www.liceo.edu.mx 是一个站点的示例,它实现了大约 1500 个标签的嵌套级别,所有标签都来自一堆 <b>。
我们只允许最多 20 个相同类型的嵌套标签,然后一起忽略它们。
bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{

无符号 i = 0;
for (HTMLStackElem* curr = m_blockStack;
         i < cMaxRedundantTagDepth && curr && curr->tagName == tagName;
     curr = curr->next, i++) { }
返回 i != cMaxRedundantTagDepth;
}

 

错位的 html 或正文结束标记

再次 - 评论不言自明。

支持真正损坏的 html。
我们从不关闭 body 标签,因为一些愚蠢的网页会在文档实际结束之前关闭它。
让我们依靠 end() 调用来关闭事物。
if (t->tagName == htmlTag || t->tagName == bodyTag )
        返回;

所以网络作者要小心——除非你想在 Webkit 容错代码中作为一个例子出现——编写格式良好的 HTML。

 

 

CSS解析

还记得介绍中的解析概念吗?好吧,与 HTML 不同,CSS 是一种上下文无关语法,可以使用介绍中描述的解析器类型进行解析。事实上,CSS 规范定义了 CSS 词法和语法语法 ( http://www.w3.org/TR/CSS2/grammar.html )。

让我们看一些例子:
词法语法(词汇)由每个标记的正则表达式定义:

注释 \/\*[^*]*\*+([^/*][^*]*\*+)*\/
数字 [0-9]+|[0-9]*"."[0-9]+
nonascii [\200-\377]
nmstart [_a-z]|{nonascii}|{escape}
nmchar [_a-z0-9-]|{nonascii}|{escape}
名称 {nmchar}+
身份 {nmstart}{nmchar}*

“ident”是标识符的缩写,就像类名一样。"name" 是一个元素 id(由 "#" 引用)

BNF 中描述了语法文法。

规则集
  : 选择器 [ ',' S* 选择器 ]*
    '{' S* 声明 [ ';' S* 声明 ]* '}' S*
  ;
选择器
  : simple_selector [ 组合选择器 | S+ [ 组合选择器 ] ]
  ;
simple_selector
  : element_name [哈希| 类 | 属性 | 伪]*
  | [哈希| 类 | 属性 | 伪]+
  ;
班级
  :'。' 身份识别
  ;
元素名称
  :身份 | '*'
  ;
属性
  : '[' S* IDENT S* [ [ '=' | 包括 | DASHMATCH ] S*
    [ 身份 | 字符串 ] S* ] ']'
  ;
  : ':' [ IDENT | 函数 S* [IDENT S*] ')' ]
  ;
说明:规则集是这样的结构:
div.error , a.error {
	红色;
	字体粗细:粗体;
}
div.error 和 a.error 是选择器。花括号内的部分包含此规则集应用的规则。此结构在此定义中正式定义:
规则集
  : 选择器 [ ',' S* 选择器 ]*
    '{' S* 声明 [ ';' S* 声明 ]* '}' S*
  ;
这意味着规则集是一个选择器或由逗号和空格分隔的可选数量的选择器(S 代表空格)。规则集包含花括号,其中包含一个声明或多个由分号分隔的可选声明。“声明”和“选择器”将在以下 BNF 定义中定义。

 

Webkit CSS 解析器

Webkit 使用Flex 和 Bison解析器生成器从 CSS 语法文件自动创建解析器。正如您在解析器介绍中所回忆的那样,Bison 创建了一个自下而上的 shift reduce 解析器。Firefox 使用手动编写的自顶向下解析器。在这两种情况下,每个 CSS 文件都被解析为一个 StyleSheet 对象,每个对象都包含 CSS 规则。CSS 规则对象包含选择器和声明对象以及其他与 CSS 语法对应的对象。


图 7:解析 CSS

 

解析脚本

这将在有关 JavaScript 的章节中处理

处理脚本和样式表的顺序

脚本

网络模型是同步的。作者希望在解析器到达 <script> 标记时立即解析和执行脚本。文档的解析停止,直到脚本被执行。如果脚本是外部的,那么必须首先从网络中获取资源——这也是同步完成的,解析会暂停,直到获取资源。这是多年来的模型,也在 HTML 4 和 5 规范中指定。作者可以将脚本标记为“延迟”,因此它不会停止文档解析并在解析后执行。HTML5 添加了一个选项来将脚本标记为异步,因此它将被不同的线程解析和执行。

推测解析

Webkit 和 Firefox 都做了这个优化。在执行脚本时,另一个线程解析文档的其余部分并找出需要从网络加载的其他资源并加载它们。这些方式可以在并行连接上加载资源,并且整体速度更好。注意 - 推测解析器不会修改 DOM 树并将其留给主解析器,它只解析对外部资源(如外部脚本、样式表和图像)的引用。

样式表

另一方面,样式表具有不同的模型。从概念上讲,由于样式表不会更改 DOM 树,因此没有理由等待它们并停止文档解析。但是,脚本在文档解析阶段要求样式信息存在问题。如果样式尚未加载和解析,脚本将得到错误的答案,显然这会导致很多问题。这似乎是一种边缘情况,但很常见。当存在仍在加载和解析的样式表时,Firefox 会阻止所有脚本。Webkit 仅在脚本尝试访问可能受卸载样式表影响的某些样式属性时才会阻止脚本。

渲染树构造

在构建 DOM 树时,浏览器会构建另一棵树,即渲染树。该树是按显示顺序排列的视觉元素。它是文档的可视化表示。此树的目的是使内容能够按正确的顺序绘制。

Firefox 将渲染树中的元素称为“框架”。Webkit 使用术语渲染器或渲染对象。
渲染器知道如何布局和绘制自己和它的孩子。
Webkits RenderObject 类,渲染器的基类,定义如下:

类渲染对象{
	虚空布局();
	虚空油漆(PaintInfo);
	虚空矩形 repaintRect();
	节点*节点;//DOM节点
	RenderStyle* 风格;// 计算样式
	RenderLayer* 包含层;//包含z-index层
}

 

每个渲染器代表一个矩形区域,通常对应于节点的 CSS 框,如 CSS2 规范所述。它包含几何信息,如宽度、高度和位置。
框类型受与节点相关的“显示”样式属性的影响(请参阅样式计算部分)。下面是根据 display 属性决定应该为 DOM 节点创建哪种类型的渲染器的 Webkit 代码。

RenderObject* RenderObject::createObject(Node* 节点,RenderStyle* 样式)
{
    文档* doc = node->document();
    RenderArena* arena = doc->renderArena();
    ...
    渲染对象* o = 0;

    开关(样式->显示()){
        情况无:
            休息;
        案例内联:
            o = 新(竞技场)RenderInline(节点);
            休息;
        案例块:
            o = 新(竞技场)RenderBlock(节点);
            休息;
        案例INLINE_BLOCK:
            o = 新(竞技场)RenderBlock(节点);
            休息;
        案例 LIST_ITEM:
            o = 新(竞技场)RenderListItem(节点);
            休息;
       ...
    }

    返回o;
}
还考虑了元素类型,例如表单控件和表格具有特殊的框架。
在 Webkit 中,如果一个元素想要创建一个特殊的渲染器,它将覆盖“createRenderer”方法。渲染器指向包含非几何信息的样式对象。

 

渲染树与 DOM 树的关系
渲染器对应于 DOM 元素,但不是一对一的关系。非可视化 DOM 元素不会被插入到渲染树中。一个例子是“head”元素。此外,其显示属性分配为“无”的元素不会出现在树中(具有“隐藏”可见性属性的元素将出现在树中)。

 

有对应于多个可视对象的 DOM 元素。这些通常是结构复杂的元素,不能用单个矩形来描述。例如,“select”元素有 3 个渲染器 - 一个用于显示区域,一个用于下拉列表框,一个用于按钮。此外,当由于一行的宽度不够而将文本分成多行时,将添加新行作为额外的渲染器。
多个渲染器的另一个示例是损坏的 HTML。根据 CSS 规范,内联元素必须仅包含块元素或仅包含内联元素。在混合内容的情况下,将创建匿名块渲染器来包装内联元素。

一些渲染对象对应于一个 DOM 节点,但不在树中的同一位置。浮点数和绝对定位的元素不流动,放置在树中的不同位置,并映射到真实框架。占位符框架是它们应该出现的地方。


图 11:渲染树和对应的 DOM 树(3.1)。“视口”是初始包含块。在 Webkit 中,它将是“RenderView”对象。
构建树的流程

在 Firefox 中,演示文稿被注册为 DOM 更新的侦听器。演示文稿将框架创建委托给“FrameConstructor”,构造函数解析样式(请参阅样式计算)并创建框架。

在 Webkit 中,解析样式和创建渲染器的过程称为“附件”。每个 DOM 节点都有一个“附加”方法。Attachment 是同步的,节点插入到 DOM 树调用新节点的“attach”方法。

处理 html 和 body 标签导致构建渲染树根。根渲染对象对应于 CSS 规范所称的包含块——包含所有其他块的最顶部的块。它的尺寸是视口——浏览器窗口显示区域的尺寸。Firefox 称之为 ViewPortFrame,Webkit 称之为 RenderView。这是文档指向的渲染对象。树的其余部分构造为 DOM 节点插入。
请参阅有关此主题的 CSS2 - http://www.w3.org/TR/CSS21/intro.html#processing-model

风格计算

构建渲染树需要计算每个渲染对象的视觉属性。这是通过计算每个元素的样式属性来完成的。

样式包括各种来源的样式表、内联样式元素和 HTML 中的视觉属性(如“bgcolor”属性)。后者被转换为匹配的 CSS 样式属性。

样式表的起源是浏览器的默认样式表、页面作者提供的样式表和用户样式表——这些是浏览器用户提供的样式表(浏览器让你定义你喜欢的样式。例如在 Firefox 中,这个通过在“Firefox Profile”文件夹中放置一个样式表来完成)。

样式计算带来了一些困难:

  1. 样式数据是一个非常大的构造,包含众多样式属性,这会导致内存问题。
  2. 如果未优化,为每个元素查找匹配规则可能会导致性能问题。遍历每个元素的整个规则列表以查找匹配项是一项繁重的任务。选择器可能具有复杂的结构,这可能会导致匹配过程从看似有希望的路径开始,但事实证明这是徒劳的,必须尝试另一条路径。
    例如 - 这个复合选择器:
    div div div div{
    ...
    }
    
    表示规则适用于作为 3 个 div 的后代的“<div>”。假设您想检查规则是否适用于给定的“<div>”元素。您选择树上的某个路径进行检查。您可能需要向上遍历节点树才能发现只有两个 div 并且规则不适用。然后您需要尝试树中的其他路径。
  3. 应用规则涉及定义规则层次结构的相当复杂的级联规则。
让我们看看浏览器如何面对这些问题:
共享风格数据

Webkit 节点引用样式对象(RenderStyle) 这些对象在某些情况下可以由节点共享。节点是兄弟姐妹或堂兄弟,并且:

  1. 元素必须处于相同的鼠标状态(例如,一个不能在 :hover 而另一个不在)
  2. 两个元素都不应该有一个 id
  3. 标签名称应该匹配
  4. 类属性应该匹配
  5. 映射属性集必须相同
  6. 链接状态必须匹配
  7. 焦点状态必须匹配
  8. 两个元素都不应受属性选择器的影响,其中受影响的定义为具有在选择器内任何位置使用属性选择器的任何选择器匹配
  9. 元素上不能有内联样式属性
  10. 必须完全没有使用同级选择器。WebCore 只是在遇到任何同级选择器时抛出一个全局开关,并在它们出现时禁用整个文档的样式共享。这包括 + 选择器和 :first-child 和 :last-child 等选择器。
Firefox 规则树

Firefox 有两个额外的树来简化样式计算 - 规则树和样式上下文树。Webkit 也有样式对象,但它们不像样式上下文树那样存储在树中,只有 DOM 节点指向其相关样式。


图 13:Firefox 风格的上下文树(2.2

 

样式上下文包含最终值。这些值是通过以正确的顺序应用所有匹配规则并执行将它们从逻辑值转换为具体值的操作来计算的。例如 - 如果逻辑值是屏幕的百分比,它将被计算并转换为绝对单位。规则树的想法真的很聪明。它可以在节点之间共享这些值以避免再次计算它们。这也节省了空间。

所有匹配的规则都存储在树中。路径中的底部节点具有更高的优先级。该树包含找到的规则匹配的所有路径。存储规则是惰性的。树不是在开始时为每个节点计算,但是每当需要计算节点样式时,计算路径都会添加到树中。

这个想法是将树路径视为词典中的单词。假设我们已经计算了这个规则树:

假设我们需要为内容树中的另一个元素匹配规则,并找出匹配的规则(以正确的顺序)是 B - E - I。我们在树中已经有了这条路径,因为我们已经计算了路径 A - B - E - I - L。我们现在要做的工作更少了。
让我们看看树是如何保存为工作的。

 

划分为结构体

样式上下文分为结构体。这些结构包含特定类别的样式信息,如边框或颜色。结构中的所有属性要么是继承的,要么是非继承的。继承属性是指除非由元素定义,否则从其父元素继承的属性。如果未定义,非继承属性(称为“重置”属性)使用默认值。

树通过在树中缓存整个结构(包含计算的最终值)来帮助我们。这个想法是,如果底部节点没有为结构提供定义,则可以使用上层节点中的缓存结构。

使用规则树计算样式上下文

在计算某个元素的样式上下文时,我们首先计算规则树中的路径或使用现有的路径。然后我们开始应用路径中的规则来填充新样式上下文中的结构。我们从路径的底部节点开始 - 具有最高优先级的节点(通常是最具体的选择器)并向上遍历树,直到我们的结构已满。如果在该规则节点中没有指定结构体,那么我们可以极大地优化——我们沿着树向上走,直到我们找到一个完全指定它的节点并简单地指向它——这是最好的优化——整个结构体是共享的。这节省了最终值的计算和内存。
如果我们找到部分定义,我们会向上走直到结构被填充。

如果我们没有找到我们的结构体的任何定义,那么如果结构体是“继承的”类型——我们在上下文树中指向我们父级的结构体,在这种情况下我们也成功地共享了结构体。如果它是一个重置结构,则将使用默认值。

如果最具体的节点确实添加了值,那么我们需要进行一些额外的计算以将其转换为实际值。然后我们将结果缓存在树节点中,以便子节点可以使用它。

如果一个元素有指向同一个树节点的兄弟或兄弟,那么整个样式上下文可以在它们之间共享。

让我们看一个例子:假设我们有这个 HTML

<html>
	<身体>
		<div class="err" id="div1">
			<p>
                          这是一个<span class="big">大错误</span>
                          这也是一个
                          <span class="big">非常大的错误</span>错误
        		</p>
		</div>
		<div class="err" id="div2">另一个错误</div>
    	</正文>
</html>
以及以下规则:
1. div {margin:5px;color:black}
2. .err {颜色:红色}
3. .big {margin-top:3px}
4. div 跨度 {margin-bottom:4px}
5. #div1 {颜色:蓝色}
6. #div 2 {颜色:绿色}

为了简化事情,假设我们只需要填写两个结构 - 颜色结构和边距结构。color 结构体只包含一个成员——颜色 margin 结构体包含四个边。
生成的规则树将如下所示(节点标有节点名称:它们指向的规则编号):


图 12:规则树

上下文树将如下所示(节点名称:它们指向的规则节点):

图 13:上下文树

假设我们解析 HTML 并获得第二个 <div> 标签。我们需要为这个节点创建一个样式上下文并填充它的样式结构。
我们将匹配规则并发现 <div> 的匹配规则是 1 ,2 和 6。这意味着树中已经存在我们元素可以使用的现有路径,我们只需要向其中添加另一个节点即可规则 6(规则树中的节点 F)。
我们将创建一个样式上下文并将其放入上下文树中。新样式上下文将指向规则树中的节点 F。

我们现在需要填充样式结构。我们将首先填写边距结构。由于最后一个规则节点(F)没有添加到边距结构中,我们可以向上走,直到找到在前一个节点插入中计算出的缓存结构并使用它。我们会在节点 B 上找到它,这是指定保证金规则的最上面的节点。

我们确实有颜色结构的定义,所以我们不能使用缓存结构。由于颜色有一个属性,我们不需要向上树来填充其他属性。我们将计算最终值(将字符串转换为 RGB 等)并将计算出的结构缓存在此节点上。

第二个 <span> 元素的工作更容易。我们将匹配规则并得出结论,它指向规则 G,就像之前的跨度一样。由于我们有指向同一个节点的兄弟节点,我们可以共享整个样式上下文,并且只指向前一个跨度的上下文。

对于包含从父级继承的规则的结构,缓存在上下文树上完成(颜色属性实际上是继承的,但 Firefox 将其视为重置并将其缓存在规则树上)。
例如,如果我们为段落中的字体添加规则:

p {font-family:Verdana;font size:10px;font-weight:bold} 
然后,作为上下文树中段落的子元素的 div 元素可以与其父元素共享相同的字体结构。这是如果没有为“div”指定字体规则。

 

在没有规则树的 Webkit 中,匹配的声明被遍历 4 次。首先应用不重要的高优先级属性(应该首先应用的属性,因为其他人依赖它们 - 例如显示),然后是高优先级重要,然后是普通优先级不重要,然后是普通优先级重要规则。这意味着多次出现的属性将根据正确的级联顺序进行解析。最后获胜。

所以总结一下 - 共享样式对象(全部或其中的一些结构)解决了问题13Firefox 规则树还有助于以正确的顺序应用属性。

操纵规则以进行轻松匹配

样式规则有多种来源:

  • CSS 规则,无论是在外部样式表中还是在样式元素中。
    p {颜色:蓝色}
    
  • 内联样式属性,如
    <p style="color:blue" />
    
  • HTML 视觉属性(映射到相关的样式规则)
    <p bgcolor="蓝色" />
    

最后两个很容易与元素匹配,因为他拥有样式属性,并且可以使用元素作为键来映射 HTML 属性。

正如之前在问题 #2 中提到的,CSS 规则匹配可能更棘手。为了解决困难,规则被操纵以便于访问。

解析样式表后,根据选择器,将规则添加到几个散列映射之一。有按 id、按类名、按标签名的映射,以及不属于这些类别的任何内容的通用映射。如果选择器是一个 id,规则将被添加到 id 映射,如果它是一个类,它将被添加到类映射等。
这种操作使得匹配规则变得更加容易。无需查看每个声明 - 我们可以从映射中提取元素的相关规则。这种优化消除了 95+% 的规则,因此在匹配过程中甚至不需要考虑它们(4.1)。

例如,让我们看看以下样式规则:

p.error {颜色:红色}
#messageDiv {height:50px}
div {边距:5px}
第一条规则将被插入到类映射中。第二个进入 id 映射,第三个进入标签映射。
对于以下 HTML 片段;
<p class="error">发生错误</p>
<div id=" messageDiv">这是一条消息</div>

 

我们将首先尝试找到 p 元素的规则。类映射将包含一个“error”键,在该键下可以找到“p.error”的规则。div元素在id映射(key为id)和标签映射中会有相关的规则。因此,剩下的唯一工作就是找出由密钥提取的哪些规则真正匹配。
例如,如果 div 的规则是

表格 div {边距:5px}
它仍然会从标签映射中提取,因为键是最右边的选择器,但它不会匹配我们没有表祖先的 div 元素。

 

Webkit 和 Firefox 都执行此操作。

以正确的级联顺序应用规则

样式对象具有对应于每个视觉属性的属性(所有 css 属性,但更通用)。如果该属性未由任何匹配的规则定义 - 那么某些属性可以由父元素样式对象继承。其他属性有默认值。

当有多个定义时,问题就开始了——这是解决问题的级联顺序。

样式表级联顺序
样式属性的声明可以出现在多个样式表中,也可以多次出现在样式表中。这意味着应用规则的顺序非常重要。这称为“级联”顺序。根据 CSS2 规范,级联顺序是(从低到高):
  1. 浏览器声明
  2. 用户正常声明
  3. 编写普通声明
  4. 作者重要声明
  5. 用户重要声明

 

浏览器声明是最不重要的,只有当声明被标记为重要时,用户才会覆盖作者。以相同的顺序声明将被排序特异性,然后它们的顺序依次确定。HTML 视觉属性被转换为匹配的 CSS 声明。它们被视为具有低优先级的作者规则。

特异性

选择器规范由CSS2 规范定义如下:

  • 如果声明来自“样式”属性而不是带有选择器的规则,则计为 1,否则计为 0 (= a)
  • 计算选择器中 ID 属性的数量(= b)
  • 计算选择器中其他属性和伪类的数量(= c)
  • 计算选择器中元素名称和伪元素的数量(= d)
连接四个数字 abcd(在具有大基数的数字系统中)给出了特殊性。

 

您需要使用的数字基数由您在其中一个类别中拥有的最高计数定义。
例如,如果 a=14 您可以使用十六进制基数。在 a=17 的不太可能的情况下,您将需要一个 17 位数字基数。后一种情况可能发生在这样的选择器上:html body div div p ...(选择器中有 17 个标签......不太可能)。

一些例子:

 * {} /* a=0 b=0 c=0 d=0 -> 特异性 = 0,0,0,0 */
 li {} /* a=0 b=0 c=0 d=1 -> 特异性 = 0,0,0,1 */
 li:first-line {} /* a=0 b=0 c=0 d=2 -> 特异性 = 0,0,0,2 */
 ul li {} /* a=0 b=0 c=0 d=2 -> 特异性 = 0,0,0,2 */
 ul ol+li {} /* a=0 b=0 c=0 d=3 -> 特异性 = 0,0,0,3 */
 h1 + *[rel=up]{} /* a=0 b=0 c=1 d=1 -> 特异性 = 0,0,1,1 */
 ul ol li.red {} /* a=0 b=0 c=1 d=3 -> 特异性 = 0,0,1,3 */
 li.red.level {} /* a=0 b=0 c=2 d=1 -> 特异性 = 0,0,2,1 */
 #x34y {} /* a=0 b=1 c=0 d=0 -> 特异性 = 0,1,0,0 */
 style="" /* a=1 b=0 c=0 d=0 -> 特异性 = 1,0,0,0 */

 

排序规则

规则匹配后,按照级联规则进行排序。Webkit 对小列表使用冒泡排序,对大列表使用合并排序。Webkit 通过覆盖规则的 ">" 运算符来实现排序:

静态布尔运算符 >(CSSRuleData& r1, CSSRuleData& r2)
{
    int spec1 = r1.selector()->specificity();
    int spec2 = r2.selector()->specificity();
    返回 (spec1 == spec2) : r1.position() > r2.position() : spec1 > spec2; 
}

 

渐进的过程

Webkit 使用一个标志来标记是否已加载所有顶级样式表(包括 @imports)。如果附加时样式未完全加载 - 使用占位符并将其标记在文档中,一旦加载样式表,它们将重新计算。

布局

当渲染器被创建并添加到树中时,它没有位置和大小。计算这些值称为布局或回流。

HTML 使用基于流的布局模型,这意味着在大多数情况下,可以一次计算几何图形。“流中”后面的元素通常不会影响“流中”较早元素的几何形状,因此布局可以在文档中从左到右、从上到下进行。也有例外——例如,HTML 表格可能需要多次传递 ( 3.5 )。

坐标系是相对于根框架的。使用顶部和左侧坐标。

布局是一个递归过程。它从根渲染器开始,它对应于 HTML 文档的元素。布局通过部分或全部帧层次结构递归地继续,为需要它的每个渲染器计算几何信息。

根渲染器的位置是 0,0,它的尺寸是视口——浏览器窗口的可见部分。

所有渲染器都有一个“布局”或“回流”方法,每个渲染器调用需要布局的子级的布局方法。

脏位系统

为了不为每一个小的改动都做一个完整的布局,浏览器使用了一个“脏位”系统。更改或添加的渲染器将自身及其子项标记为“脏” - 需要布局。

有两个标志——“脏”和“孩子脏”。孩子脏意味着虽然渲染器本身可能没问题,但它至少有一个孩子需要布局。

全局和增量布局

布局可以在整个渲染树上触发 - 这是“全局”布局。这可能是由于:

  1. 影响所有渲染器的全局样式更改,例如字体大小更改。
  2. 由于屏幕被调整大小

 

布局可以是增量的,只有脏的渲染器才会被布局(这可能会造成一些损坏,需要额外的布局)。
当渲染器变脏时(异步)触发增量布局。例如,当额外的内容来自网络并被添加到 DOM 树之后,新的渲染器被附加到渲染树。


图 20:增量布局 - 仅布局脏渲染器及其子级(3.6)。

 

异步和同步布局

增量布局是异步完成的。Firefox 为增量布局排队“回流命令”,调度程序触发这些命令的批量执行。Webkit 也有一个执行增量布局的计时器 - 遍历树并且“脏”渲染器被布局出来。
请求样式信息的脚本,如“offsightHeight”,可以同步触发增量布局。
全局布局通常会被同步触发。
有时布局会在初始布局后作为回调触发,因为某些属性(例如滚动位置)发生了变化。

优化

如果布局是由“大小调整”或呈现器的位置的变化(而不是大小)触发,使得大小从缓存中获取和无需重新计算。
在某些情况下-只有一个子树被修改和布局不从根开始。这可能发生在更改是局部的并且不影响其周围环境的情况下 - 例如插入文本字段的文本(否则每次击键都会触发从根开始的布局)。

 

布局过程

布局通常有以下模式:

  1. 父渲染器确定自己的宽度。
  2. 父母检查孩子并且:
    1. 放置子渲染器(设置其 x 和 y)。
    2. 如果需要,调用子布局(它们很脏,或者我们处于全局布局或其他原因) - 这会计算孩子的高度。
  3. 父级使用子级累积高度以及边距和填充的高度来设置它自己的高度 - 这将由父级渲染器的父级使用。
  4. 将其脏位设置为 false。

 

Firefox 使用“状态”对象(nsHTMLReflowState)作为布局参数(称为“回流”)。其中状态包括父母宽度。
Firefox 布局的输出是一个“度量”对象(nsHTMLReflowMetrics)。它将包含渲染器计算的高度。

宽度计算

渲染器的宽度是使用容器块的宽度、渲染器的样式“宽度”属性、边距和边框来计算的。
例如以下 div 的宽度:

<div style="width:30%"/>
将由 Webkit 计算如下(类 RenderBox 方法 calcWidth):
  • 容器宽度是容器 availableWidth 和 0 中的最大值。在这种情况下,availableWidth 是 contentWidth,计算如下:
    clientWidth() - paddingLeft() - paddingRight()
    
    clientWidth 和 clientHeight 表示对象的内部,不包括边框和滚动条。
  • 元素宽度是“宽度”样式属性。它将通过计算容器宽度的百分比来计算为绝对值。
  • 现在添加了水平边框和填充。
到目前为止,这是“首选宽度”的计算。现在将计算最小和最大宽度。
如果首选宽度高于最大宽度 - 使用最大宽度。如果它低于最小宽度(最小的牢不可破的单位),则使用最小宽度。

 

这些值被缓存,以防需要布局但宽度不会改变。

 

断线

当布局中间的渲染器决定需要中断时。它停止并传播到它的父级它需要被打破。父级将创建额外的渲染器并对其调用布局。

绘画

在绘制阶段,遍历渲染树并调用渲染器的“paint”方法以在屏幕上显示其内容。绘画使用 UI 基础结构组件。更多关于 UI 的章节。

全局和增量

像布局一样,绘制也可以是全局的——整个树都被绘制了——或者是增量的。在增量绘制中,某些渲染器的更改不会影响整个树。更改后的渲染器使其在屏幕上的矩形无效。这会导致操作系统将其视为“脏区域”并生成“绘制”事件。操作系统巧妙地做到了这一点,并将多个区域合并为一个区域。在 Chrome 中,它更复杂,因为渲染器位于与主进程不同的进程中。Chrome 在一定程度上模拟了操作系统的行为。演示文稿侦听这些事件并将消息委托给渲染根。遍历树直到到达相关的渲染器。它会重新粉刷自己(通常是它的孩子)。

涂装顺序

CSS2 定义了绘制过程的顺序 - http://www.w3.org/TR/CSS21/zindex.html这实际上是元素在堆叠上下文中堆叠的顺序此顺序会影响绘制,因为堆栈是从后向前绘制的。块渲染器的堆叠顺序是:
  1. 背景颜色
  2. 背景图
  3. 边界
  4. 孩子们
  5. 大纲

 

火狐显示列表

Firefox 遍历渲染树并为绘制的矩形构建一个显示列表。它包含与矩形相关的渲染器,按正确的绘制顺序(渲染器的背景,然后是边框等)。
这样,树只需遍历一次即可重绘而不是多次 - 绘制所有背景,然后绘制所有图像,然后绘制所有边框等
。Firefox 通过不添加将被隐藏的元素来优化该过程,例如完全在其他不透明元素下方的元素元素。

 

Webkit 矩形存储

在重绘之前,webkit 将旧矩形保存为位图。然后它只绘制新矩形和旧矩形之间的增量。

动态变化

浏览器会尝试做最少的操作来响应更改。因此,对元素颜色的更改只会导致元素重新绘制。对元素位置的更改将导致元素、其子元素和可能的兄弟元素的布局和重绘。添加一个 DOM 节点会导致节点的布局和重绘。主要的变化,比如增加“html”元素的字体大小,将导致缓存失效、依赖和整个树的重绘。

渲染引擎的线程

渲染引擎是单线程的。除了网络操作之外,几乎所有事情都发生在单个线程中。在 Firefox 和 safari 中,这是浏览器的主线程。在 chrome 中,它是选项卡进程主线程。
网络操作可以由多个并行线程执行。并行连接的数量是有限的(通常为 2 - 6 个连接。例如 Firefox 3,使用 6 个)。

事件循环

浏览器主线程是一个事件循环。它是一个无限循环,使过程保持活力。它等待事件(如布局和绘制事件)并处理它们。这是主事件循环的 Firefox 代码:
而 (!mExiting)
    NS_ProcessNextEvent(线程);

CSS2 视觉模型

画布

根据CCS2 规范,术语画布描述了“呈现格式结构的空间”。- 浏览器绘制内容的地方。
对于空间的每个维度,画布都是无限的,但浏览器会根据视口的维度选择初始宽度。

根据http://www.w3.org/TR/CSS2/zindex.html,如果包含在另一个画布中,画布是透明的,如果不是,则给出浏览器定义的颜色。

CSS盒模型

CSS盒模型描述了为在文档树元件产生并根据该可视化格式模型中规定的矩形框。
每个框都有一个内容区域(例如,文本、图像等)和可选的周围填充、边框和边距区域。


图 14:CSS2 盒子模型

 

每个节点生成 0..n 个这样的框。
所有元素都有一个“显示”属性,用于确定它们将生成的框的类型。
例子:

block - 生成一个块框。
inline - 生成一个或多个内联框。
none - 不生成框。
默认是内联的,但浏览器样式表设置了其他默认值。例如 - “div”元素的默认显示是块。
您可以在此处找到默认样式表示例http://www.w3.org/TR/CSS2/sample.html

 

定位方案

共有三种方案:

  1. 正常 - 对象根据其在文档中的位置进行定位 - 这意味着它在渲染树中的位置就像它在 dom 树中的位置一样,并根据其框类型和尺寸进行布局
  2. 浮动 - 对象首先像正常流一样布置,然后尽可能向左或向右移动
  3. 绝对 - 对象在渲染树中的放置与其在 DOM 树中的位置不同

 

定位方案由“position”属性和“float”属性设置。

  • 静态和相对导致正常流动
  • 绝对和固定导致绝对定位

在静态定位中,没有定义位置并使用默认定位。在其他方案中,作者指定了位置——上、下、左、右。

 

框的布局方式由以下因素决定:

  • 箱型
  • 箱体尺寸
  • 定位方案
  • 外部信息 - 如图像大小和屏幕大小

 

箱型

块框:形成一个块 - 在浏览器窗口上有自己的矩形。


图 15:方块方块

 

内联框:没有自己的块,但在包含块内。


图 15:Inine 框

 

块一个接一个地垂直格式化。内联被水平格式化。


图 16:块和内联格式

 

内联框放置在行或“行框”内。这些线至少与最高的盒子一样高,但可以更高,当盒子对齐“基线”时 - 意味着元素的底部对齐在另一个盒子的点而不是底部。如果容器宽度不够,内联会分成几行。这通常是在一个段落中发生的事情。


图 17:线条

 

定位

相对的

相对定位 - 像往常一样定位,然后按所需的增量移动。


图 18:相对定位

 

浮动

浮动框移动到行的左侧或右侧。有趣的特性是其他框围绕它流动的 HTML:

<p>
<img style="float:right" src="images/image.gif" width="100" height="100">Lorem ipsum dolor sat amet, consectetuer...
</p>
看起来像:

图 19:浮动

 

绝对和固定

无论正常流程如何,布局都是准确定义的。该元素不参与正常流。尺寸是相对于容器而言的。在固定 - 容器是视口。


图 20:固定定位

注意 - 即使滚动文档,固定框也不会移动!

 

分层表示

它由 z-index CSS 属性指定。它表示框的第三维,即它沿“z 轴”的位置。

盒子被分成堆栈(称为堆栈上下文)。在每个堆栈中,后向元素将首先被绘制,前向元素在顶部,更接近用户。在重叠的情况下,将隐藏前一个元素。
堆栈根据 z-index 属性进行排序。具有“z-index”属性的框形成本地堆栈。视口具有外部堆栈。
例子:

<STYLE type="text/css">
      div { 
        位置:绝对; 
        左:2in; 
        顶部:2 英寸; 
      }
    </样式>

  <P>   
	    <DIV 
	         style="z-index: 3; width: 1in; height: 1in;">
	    </DIV>
	    <DIV
	         style="z-index: 1;width: 2in; height: 2in;">
	    </DIV>
   </p>
结果将是这样的:

图 20:固定定位

 

虽然绿色的 div 在红色的 div 之前,并且在常规流中之前会被绘制,但 z-index 属性更高,因此它在根框持有的堆栈中更靠前。

资源

  1. 浏览器架构
    1. 格罗斯库特,艾伦。Web 浏览器的参考架构。http://grosskurth.ca/papers/browser-refarch.pdf。
  2. 解析
      1. Aho、Sethi、Ullman,编译器:原则、技术和工具(又名“龙之书”),Addison-Wesley,1986
    1. 里克·杰利夫。大胆而美丽:HTML 5 的两个新草案。http://broadcast.oreilly.com/2009/05/the-bold-and-the-beautiful-two.html。
  3. 火狐
    1. L. David Baron,更快的 HTML 和 CSS:Web 开发人员的布局引擎内部结构。http://dbaron.org/talks/2008-11-12-faster-html-and-css/slide-6.xhtml。
    2. L. David Baron,更快的 HTML 和 CSS:Web 开发人员的布局引擎内部结构(Google 技术谈话视频)。http://www.youtube.com/watch?v=a2_6bGNZ7bA。
    3. L. David Baron,Mozilla 的布局引擎。http://www.mozilla.org/newlayout/doc/layout-2006-07-12/slide-6.xhtml。
    4. L. David Baron,Mozilla 风格系统文档。http://www.mozilla.org/newlayout/doc/style-system.html。
    5. Chris Waterson,关于 HTML 重排的注释。http://www.mozilla.org/newlayout/doc/reflow.html。
    6. 克里斯沃特森,壁虎概述。http://www.mozilla.org/newlayout/doc/gecko-overview.htm。
    7. Alexander Larsson,HTML HTTP 请求的生命周期。https://developer.mozilla.org/en/The_life_of_an_HTML_HTTP_request。
  4. 网络套件
    1. David Hyatt,实现 CSS(第 1 部分)。http://weblogs.mozillazine.org/hyatt/archives/cat_safari.html。
    2. David Hyatt,WebCore 概述。http://weblogs.mozillazine.org/hyatt/WebCore/chapter2.html。
    3. David Hyatt,WebCore 渲染。http://webkit.org/blog/114/。
    4. David Hyatt,FOUC 问题。http://webkit.org/blog/66/the-fouc-problem/。
  5. W3C 规范
    1. HTML 4.01 规范。http://www.w3.org/TR/html4/。
    2. HTML5 规范。http://dev.w3.org/html5/spec/Overview.html。
    3. 级联样式表级别 2 修订版 1 (CSS 2.1) 规范。http://www.w3.org/TR/CSS2/。
  6. 浏览器构建说明
    1. 火狐。https://developer.mozilla.org/en/Build_Documentation
    2. 网络套件。http://webkit.org/building/build.html

Web开发中需要了解的东西 | 酷 壳 - CoolShell

posted @ 2021-12-30 15:19  CharyGao  阅读(114)  评论(0编辑  收藏  举报