浏览器的工作方式

现代网络浏览器的幕后故事

Tali Garsiel
Tali Garsiel

序言

这是一部介绍 WebKit 和 Gecko 内部操作的综合入门文档, 这得益于以色列开发者 Tali Garsiel 的大量研究。超过几个 她查阅了所有已发布的关于浏览器内部机制的数据, 会花大量时间阅读网络浏览器源代码她写道:

作为一名 Web 开发者,了解浏览器运维的内部原理 有助于您做出更明智的决策,并了解开发背后的正当理由 最佳做法。虽然这是一个相当长的文档, 请花些时间深入了解相关信息。您会很高兴。

Paul Ireland,Chrome 开发技术推广部

简介

网络浏览器是使用最广泛的软件。在这篇入门指南中,我介绍了 它们都是在后台发挥作用。我们将了解输入 google.com 后会发生什么情况 ,直到您在浏览器屏幕上显示 Google 页面。

我们要讨论的浏览器

现如今,桌面设备上使用的主流浏览器有五种:Chrome 浏览器、Internet Explorer、Firefox、Safari 和 Opera。在移动设备上,主要的浏览器包括 Android 浏览器、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%。

浏览器的主要功能

浏览器的主要功能是向服务器请求并显示您选择的网络资源,并将其显示在浏览器窗口中。 资源通常是 HTML 文档,但也可以是 PDF、图片或其他类型的内容。 资源的位置由用户使用 URI(统一资源标识符)指定。

浏览器解释和显示 HTML 文件的方式在 HTML 和 CSS 规范中指定。 这些规范由网络标准组织 W3C(万维网联盟)进行维护。多年来,各浏览器都只遵守了这些规范的一部分,并且一直在开发自己的扩展程序。这给 Web 开发者带来了严重的兼容性问题。如今,大部分浏览器都或多或少地遵循了规范。

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

  1. 用于插入 URI 的地址栏
  2. “后退”和“前进”按钮
  3. 书签选项
  4. 用于刷新或停止加载当前文档的“刷新”和“停止”按钮
  5. 用于转到首页的“主页”按钮

奇怪的是,浏览器的用户界面并没有任何正式的规范,这是多年来积累的优秀实践以及浏览器彼此模仿的结果。 HTML5 规范没有定义浏览器必须具有的界面元素,但列出了一些常见的元素。其中包括地址栏、状态栏和工具栏。 当然,有些功能是特定浏览器所独有的,比如 Firefox 的下载管理器。

高层级基础设施

浏览器的主要组件包括:

  1. 界面:包括地址栏、前进/后退按钮、书签菜单等。除显示请求网页的窗口外,浏览器显示的其他部分。
  2. 浏览器引擎:在界面和渲染引擎之间编制操作。
  3. 呈现引擎:负责显示请求的内容。例如,如果请求的内容是 HTML,呈现引擎会解析 HTML 和 CSS,并在屏幕上显示解析后的内容。
  4. 网络:对于网络调用,例如 HTTP 请求,在独立于平台的接口后针对不同平台使用不同的实现。
  5. 界面后端:用于绘制基本微件,例如组合框和窗口。此后端公开了与平台无关的通用接口。在其底层使用操作系统界面方法。
  6. JavaScript 解释器。用于解析和执行 JavaScript 代码。
  7. 数据存储。这是一个持久层。浏览器可能需要在本地保存各种数据,例如 Cookie。浏览器还支持多种存储机制,如 localStorage、IndexedDB、WebSQL 和 FileSystem。
。 <ph type="x-smartling-placeholder">
</ph> 浏览器组件
图 1:浏览器组件

请务必注意,Chrome 等浏览器会运行多个呈现引擎实例:每个标签页对应一个实例。每个标签页都是在单独的进程中运行。

呈现引擎

呈现引擎的责任当然是...呈现,也就是在浏览器屏幕上显示请求的内容。

默认情况下,呈现引擎可以显示 HTML 和 XML 文档及图片。 可通过插件或扩展程序显示其他类型的数据;例如,使用 PDF 查看器插件显示 PDF 文档。不过在本章中,我们将重点关注主要用例:显示使用 CSS 设置格式的 HTML 和图片。

不同的浏览器使用不同的呈现引擎:Internet Explorer 使用 Trident,Firefox 使用 Gecko,Safari 使用 WebKit。Chrome 和 Opera(15 版及更高版本)使用 Blink(WebKit 的一个分支)。

WebKit 是一个开源渲染引擎,最初是作为 Linux 平台的引擎,后来经过 Apple 修改,以支持 Mac 和 Windows。

主流程

呈现引擎将开始获取所请求的文档的内容 从网络层开始。通常,该过程以 8kB 数据块为单位。

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

呈现引擎的基本流程
图 2:呈现引擎的基本流程

呈现引擎会开始解析 HTML 文档,并将元素转换为名为“内容树”的树中的 DOM 节点。引擎将会解析外部 CSS 文件和样式元素中的样式数据。HTML 中的样式信息与视觉说明将用于创建另一个树:渲染树

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

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

需要注意的是,这是一个渐进的过程。为获得更好的用户体验,呈现引擎会尽可能尽快在屏幕上显示内容。 它不会等到所有 HTML 都解析完毕后,才会开始构建呈现树并为其设置布局。 系统会解析并显示部分内容,同时继续处理来自网络的其余内容。

主流程示例

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

从图 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 + +”不符合任何规则,因此是无效的输入。

词汇和语法的正式定义

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

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

INTEGER: 0|[1-9][0-9]*
PLUS: +
MINUS: -

如您所见,整数由正则表达式定义。

语法通常使用 BNF 格式定义。 我们的语言定义为:

expression :=  term  operation  term
operation :=  PLUS | MINUS
term := INTEGER | expression

之前说过,如果某个语言的语法是与上下文无关的语法,那么它就可以由常规解析器进行解析。 与上下文无关的语法的直观定义是指可以完全用 BNF 格式表达的语法。 有关正式定义,请参阅 维基百科中关于与上下文无关的语法的文章

解析器类型

有两种类型的解析器:自上而下解析器和自下而上解析器。一种直观的解释是,自上而下的解析器会检查语法的高级结构,并尝试查找规则匹配。自下而上的解析器从输入开始,再逐步将其转换为语法规则,从低级别的规则开始,直到满足高级别的规则。

我们来看看这两种解析器如何解析我们的示例。

自上而下的解析器将从更高级别的规则开始:它会将 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 组织制定的规范中定义。

正如我们在解析简介中所了解到的,语法可以使用 BNF 等格式进行正式定义。

遗憾的是,所有的常规解析器都不适用于 HTML(我并不是开玩笑,它们会用于解析 CSS 和 JavaScript)。 HTML 无法用解析器所需的与上下文无关的语法轻松定义。

有一种定义 HTML 的正式格式,即 DTD(文档类型定义),但它不是与上下文无关的语法。

乍一看,这似乎很奇怪:HTML 与 XML 非常相似。有很多 XML 解析器可以使用。 HTML 存在一个 XML 变体 (XHTML),那么有什么最大的区别呢?

不同之处在于 HTML 方法更加“宽容”:它允许您省略某些标记(然后以隐式方式添加),或者有时省略开始或结束标记,等等。 总的来说,这是一个“软性”而不是 XML 严格的语法。

这些看似微不足道的小细节大不相同。 一方面,这是 HTML 如此流行的主要原因:它能包容您的错误,并为网络作者简化工作。 另一方面,这使得它很难编写正式的语法。总而言之,传统解析器无法轻松地解析 HTML,因为其语法与上下文无关。XML 解析器无法解析 HTML。

HTML DTD

HTML 定义采用的是 DTD 格式。此格式用于定义 SGML 系列的语言。该格式包含所有允许使用的元素及其属性和层次结构的定义。如前所述,HTML DTD 无法构成与上下文无关的语法。

DTD 存在一些变体。严格模式完全遵守规范,但其他模式支持过去的浏览器所使用的标记。这样做的目的是向后兼容旧内容。 当前的严格 DTD 如下: www.w3.org/TR/html4/strict.dtd

DOM

输出树(“解析树”)是由 DOM 元素和属性节点组成的树。 DOM 是文档对象模型 (Document Object Model) 的缩写。 它是 HTML 文档的对象表示,以及 HTML 元素与外部世界(如 JavaScript)之间的接口。

树的根是“Document”对象。

DOM 与标记之间几乎是一一对应的关系。 例如:

<html>
  <body>
    <p>
      Hello World
    </p>
    <div> <img src="example.png"/></div>
  </body>
</html>

该标记将转换为以下 DOM 树:

示例标记的 DOM 树
图 8:示例标记的 DOM 树

与 HTML 一样,DOM 也是由 W3C 组织指定的。 请参阅 www.w3.org/DOM/DOMTR。 它是关于文档操作的通用规范。特定模块描述了特定于 HTML 的元素。HTML 定义可参见此处: 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 标记包括开始标记、结束标记、属性名称和属性值。

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

HTML 解析流程(摘自 HTML5 规范)
图 9:HTML 解析流程(摘自 HTML5 规范)

标记化算法

该算法的输出是 HTML 标记。 该算法用状态机表示。 每种状态都会使用输入流的一个或多个字符,并根据这些字符更新下一个状态。 当前的标记化状态和树构建状态会影响到该决定。这意味着,接收的字符相同,针对正确的下一个状态会产生不同的结果,具体取决于当前状态。 该算法过于复杂,无法完整描述,我们来看一个简单的示例,这有助于我们理解其原理。

基本示例 - 将以下 HTML 标记化:

<html>
  <body>
    Hello world
  </body>
</html>

初始状态是“数据状态”。 当遇到 < 字符时,状态会更改为“标记打开状态”。 接收一个 a-z 字符会创建“起始标记令牌”,状态会更改为“标记名称状态”。 我们会保持这种状态,直到使用 > 字符为止。每个字符都会附加到新词法单元名称。在本例中,创建的令牌是 html 令牌。

当达到 > 标记时,系统会发出当前令牌,状态变回“数据状态”<body> 标记也会执行相同的步骤。 到目前为止,htmlbody 标记已发出。现在我们回到“数据状态”。 接收 Hello worldH 字符会导致创建并发出字符令牌,一直持续到达到 </body>< 为止。我们将为 Hello world 的每个字符发出一个字符令牌。

现在,我们回到“代码打开状态”。 使用下一个输入 / 将创建 end tag token 并进入“标记名称状态”。我们同样会保持此状态,直到达到 >。然后,系统会发出新的标记令牌,并回到“数据状态”</html> 输入的处理方式与上一种情况相同。

对示例输入进行标记化
图 10:对示例输入进行标记化

树构建算法

在创建解析器时,会创建 Document 对象。在树构建阶段,以 Document 为根节点的 DOM 树将会修改,并向其中添加元素。 标记生成器发送的每个节点都会由树构造函数进行处理。 对于每个标记,规范定义了与其相关的 DOM 元素,并将为此标记创建相应的 DOM 元素。 元素会添加到 DOM 树以及打开的元素堆栈中。 此堆栈用于纠正嵌套不匹配和未关闭的标记。 该算法也可以用状态机来描述。这些状态称为“插入模式”。

我们来看一下示例输入的树构建过程:

<html>
  <body>
    Hello world
  </body>
</html>

树构建阶段的输入是来自标记化阶段的词元序列。 第一种模式是“初始模式”。接收“html”令牌转至 "before html" 模式,并在该模式下重新处理令牌。 这样,系统就会创建附加在 Document 根对象的 HTMLHtmlElement 元素。

状态将更改为 "before head"。“body”令牌。虽然我们没有“head”元素,但系统会隐式创建 HTMLHeadElement系统会将该标记添加到树中。

现在进入 "in head" 模式,然后进入 "after head" 模式。系统会重新处理 body 令牌,创建并插入 HTMLBodyElement,并将模式转换为“in body”

“Hello World”的字符标记字符串。第一个代码会导致创建和插入“Text”并将其他字符附加到该节点。

接收 body 结束标记会切换到 "after body" 模式。 现在,我们将收到 HTML 结束标记,进入到“after after body”模式。 收到文件结束标记会结束解析。

示例 HTML 的树构建。
图 11:示例 HTML 的树构建

解析完成后的操作

在此阶段,浏览器会将文档标记为可交互,并开始解析处于“deferred”的脚本。模式:应在解析文档后执行的请求。 然后,文档状态将设置为“完成”和“load”事件。

您可以在 HTML5 规范中查看标记化和树构建的完整算法

浏览器的容错性

您绝不会看到“语法无效”的消息错误。 浏览器会修复所有无效内容,然后继续运行。

以下面的 HTML 为例:

<html>
  <mytag>
  </mytag>
  <div>
  <p>
  </div>
    Really lousy HTML
  </p>
</html>

我必须违反了大约一百万条规则(“mytag”不是标准标记,“p”和“div”元素的嵌套错误等等),但浏览器仍然能正确显示这些内容,而且毫无疑问。 因此,很多解析器代码都会修复 HTML 作者的错误。

各种浏览器的错误处理机制非常一致,但令人惊讶的是,这种处理方式并不适用于 HTML 规范。 与添加书签和前进/后退按钮一样,它也是浏览器在多年发展中的产物。已知无效 HTML 结构在许多网站上重复出现,这些浏览器会尝试按照其他浏览器的方式来修复这些元素。

HTML5 规范定义了其中一些要求。(WebKit 在 HTML 解析器类开头的注释中对这一点进行了很好的总结。)

解析器对标记化输入进行解析,以构建文档树。如果文档格式正确,解析就很简单了。

遗憾的是,我们必须处理许多格式不正确的 HTML 文档,因此解析器必须容忍错误。

我们至少要处理以下错误情况:

  1. 明显不能在某些外部标记中添加的元素。在这种情况下,我们应先关闭所有标记,直至标记禁止添加该元素,然后再添加该标记。
  2. 我们不能直接添加该元素。这可能是因为撰写文档的人忘记了其中的一些标记(或者其中的标记是可选的)。以下标记可能属于这种情况:HTML HEAD BODY TBODY TR TD LI(我忘了任何标记了吗?)。
  3. 我们想在 inline 元素内添加一个 block 元素。关闭所有内嵌元素,直至下一个更高级别的 block 元素。
  4. 如果这样没有帮助,请关闭元素,直到我们允许添加元素为止,或者忽略该标记。

我们来看一些 WebKit 容错的示例:

</br> 代替 <br>

有些网站使用 </br> 而不是 <br>。为了与 IE 和 Firefox 兼容,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);

WebKit 使用堆栈来存储当前的元素内容:它会从外部表格堆栈中弹出内部表格。现在,这两个表将成为同级表。

嵌套的表单元素

如果用户在一个表单内放入另一个表单,则第二种表单会被忽略。

代码:

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

代码层次结构过深

注释已经说明了一切。

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 或正文结束标记

再强调一遍 - 评论已说明了一切。

if (t->tagName == htmlTag || t->tagName == bodyTag )
        return;

因此,网页作者请注意,除非您想在 WebKit 容错代码段中显示为示例,否则请编写格式正确的 HTML。

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}*

&quot;ident&quot;是标识符的缩写,例如类名称。 “名称”是元素 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.errora.error 是选择器。大括号内的部分包含由此规则集应用的规则。 此结构的正式定义如下:

ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;

这意味着规则集是一个选择器,或者多个由英文逗号和空格(S 表示空格)分隔的选择器(可选)。 规则集包含大括号,以及其中的一个声明,或者多个由分号分隔的声明(可选)。 “声明”和“selector”将在以下 BNF 定义中进行定义。

WebKit CSS 解析器

WebKit 会使用 Flex 和 Bison 解析器生成器,根据 CSS 语法文件自动创建解析器。 正如解析器简介中所提到的,Bison 创建了一个自下而上的移位归约解析器。 Firefox 使用的是手动编写的自上而下的解析器。 在这两种情况下,每个 CSS 文件都会解析为 StyleSheet 对象。每个对象都包含 CSS 规则。CSS 规则对象包含选择器和声明对象,以及其他与 CSS 语法对应的对象。

解析 CSS。
图 12:解析 CSS

脚本和样式表的处理顺序

脚本

网络的模型是同步的。作者希望解析器遇到 <script> 标记时立即解析并执行脚本。 文档解析将停止,直到脚本执行完毕。 如果脚本是外部的,则必须先从网络中提取资源(该操作也是同步完成的),解析将会停止,直到资源提取完毕。 此模型已经使用了很多年,在 HTML4 和 5 规范中也有所指定。 作者可以将“推迟”属性添加到脚本中,在这种情况下,它不会停止文档解析,而是在文档解析完毕后执行。HTML5 添加了一个选项,可将脚本标记为异步,以便由其他线程解析和执行。

推测解析

WebKit 和 Firefox 都进行了这项优化。在执行脚本时,另一个线程会解析文档的其余部分,找出需要从网络加载的其他资源并进行加载。通过这种方式,可以在并行连接上加载资源,从而提高整体速度。请注意:推测解析器仅解析对外部资源(如外部脚本、样式表和图片)的引用:它不会修改 DOM 树,而是由主解析器决定的。

样式表

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

渲染树构建

在 DOM 树构建的同时,浏览器还会构建另一个树结构,即渲染树。 这种树由视觉元素按照显示顺序组成。 它是文档的可视化表示。 此树的用途是让您能够以正确的顺序绘制内容。

Firefox 将呈现树中的元素称为“框架”。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 规范中所述。 包含宽度、高度和位置等几何信息。

方框类型受“显示”选项的影响与该节点相关的样式属性的值(参见样式计算部分)。 以下 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 元素不会插入呈现树中。例如,“head”元素。显示值被指定为“none”的元素不会显示在树中(而可见性为“隐藏”的元素会显示在树中)。

有一些 DOM 元素对应多个可视化对象。这些元素通常是具有复杂结构的元素,无法用单个矩形来描述。例如,“select”元素有三个呈现器:一个用于显示区域,一个用于下拉列表框,另一个用于按钮。 此外,如果文本因宽度不足以容纳一行而分成多行,新行将会作为额外的渲染程序添加。

另一个关于多个呈现器的示例是损坏的 HTML。 根据 CSS 规范,inline 元素只能包含 block 元素或 inline 元素中的一种。 对于混合内容,系统会创建匿名块渲染程序来封装内嵌元素。

有些渲染对象对应于 DOM 节点,但在树中的位置与 DOM 节点不同。 浮动定位和绝对定位的元素就是这样,它们会置于代码树的不同部分,并映射到真实的框架中。 它们本应位于占位符框架的位置。

呈现树和对应的 DOM 树。
图 13:渲染树和对应的 DOM 树。“视口”是初始包含块。在 WebKit 中对象

构建树的流程

在 Firefox 中,系统会将呈现方式注册为 DOM 更新监听器。 展示层将框架创建工作委托给 FrameConstructor,构造函数会解析样式(请参阅样式计算)并创建框架。

在 WebKit 中,解析样式和创建渲染程序的过程称为“附加”。 每个 DOM 节点都有一个“附加”方法。 附加是同步进行的,向 DOM 树插入节点会调用新节点“附加”方法。

处理 html 和 body 标记会导致构建呈现树根。 根渲染对象对应于 CSS 规范所称的包含块:最顶层的块,包含所有其他块。它的尺寸就是视口,即浏览器窗口显示区域的尺寸。 Firefox 将其称为 ViewPortFrame,而 WebKit 则将其称为 RenderView。 这是文档指向的渲染对象。 树的其余部分以 DOM 节点插入的形式进行构建。

请参阅处理模型的 CSS2 规范

样式计算

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

样式包括各种来源的样式表、内嵌样式元素以及 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 节点指向其相关样式。

Firefox 样式上下文树。
图 14:Firefox 样式上下文树。

样式上下文包含结束值。这些值的计算方法是,以正确的顺序应用所有匹配规则,并执行将逻辑值转换为具体值的操作。例如,如果逻辑值是屏幕大小的百分比,则计算该逻辑值并将其转换为绝对单位。 规则树的点子真的很巧妙,它支持在节点之间共享这些值,以避免重复计算。这还可以节省空间。

所有匹配的规则都存储在树中。路径中底部节点的优先级较高。 规则树中包含所找到的规则匹配项的所有路径。 规则的存储是延迟进行的。该树不会在开始时针对每个节点进行计算,但每当需要计算节点样式时,都会将计算的路径添加到树中。

其思路是将树路径视为词典中的单词。 假设我们已经计算出了此规则树:

计算的规则树
图 15:计算出的规则树。

假设我们需要为内容树中的另一个元素匹配规则,并发现匹配的规则(按正确顺序)为 B-E-I。树中已经有了这个路径,因为我们已经计算了路径 A-B-E-I-L。现在,我们要做的工作就少了。

我们来看看树是如何为我们的工作省钱的。

结构划分

样式上下文可分为结构体。这些结构体包含特定类别(如 border 或 color)的样式信息。结构体中的所有属性要么都是继承的,要么不是继承的。继承属性是指从元素父项继承而来(除非由元素定义)的属性。非继承的属性(称为“重置”属性)如果未进行定义,将使用默认值。

该树帮助我们在树中缓存整个结构体(包含计算出的结束值)。其理念是,如果底部节点没有提供结构体的定义,则可以使用较高节点中的缓存结构体。

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

在计算特定元素的样式上下文时,我们首先计算规则树中的路径,或使用现有的路径。 然后,我们开始应用路径中的规则,以填充新样式上下文中的结构。我们从路径中具有最高优先级的底部节点(通常是最具体的选择器)开始,并向上遍历规则树,直至结构体填满。 如果该规则节点中没有针对结构体的规范,那么我们可以进行大幅优化 - 我们沿着树往上走,直到找到能够完整指定该结构并指向该结构的节点 - 这是最好的优化方法 - 整个结构都是共享的。 这可以节省结束值的计算和内存。

如果我们找到了部分定义,就会向上遍历布局树,直到结构填充完毕。

如果我们找不到任何结构体的任何定义,那么如果该结构体是“继承的”类型时,我们指向上下文树中父项的结构体。在本例中,我们还成功共享了结构体。 如果是重置结构体,将使用默认值。

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

如果某个元素与同级元素都指向同一个树节点,那么这些元素之间就可以共享整个样式上下文

我们来看一个示例: 假设我们有以下 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>

以及以下规则:

div {margin: 5px; color:black}
.err {color:red}
.big {margin-top:3px}
div span {margin-bottom:4px}
#div1 {color:blue}
#div2 {color:green}

为了简化操作,假设我们只需要填充两个结构体:color 结构和 margin 结构。 color 结构仅包含一个成员:color [颜色] margin 结构包含四条边。

生成的规则树将如下所示(节点标有节点名称:它们所指向的规则的编号):

规则树
图 16:规则树

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

上下文树。
图 17:上下文树

假设我们解析 HTML 时遇到了第二个 <div> 标记,我们需要为此节点创建样式上下文,并填充其样式结构。

通过匹配规则,我们发现 <div> 的匹配规则是 1、2 和 6。 这意味着,树中已经有一个可供我们的元素使用的路径,我们只需为规则 6(规则树中的节点 F)再添加一个节点即可。

我们将创建一个样式上下文,并将其放入上下文树中。新样式上下文将指向规则树中的节点 F。

现在,我们需要填充样式结构。首先,我们要填充一个 margin 结构。 由于最后一个规则节点 (F) 没有添加到 margin 结构,我们可以向上查找树,直到找到在之前的节点插入中计算过的缓存结构,然后使用该结构。 我们会在节点 B 上找到它,该节点是指定了外边距规则的最高节点。

我们已经有了 color 结构的定义,因此无法使用缓存的结构。 由于 color 有一个属性,我们无需向上绘制颜色树以填充其他属性。 我们将计算结束值(将字符串转换为 RGB 等),并在此节点上缓存计算的结构。

第二个 <span> 元素的工作更加简单。我们将匹配规则,并得出与之前的 span 一样指向规则 G 的结论。 由于我们的同级元素指向同一节点,因此我们可以共享整个样式上下文,并且只需指向上一个 span 的上下文即可。

对于包含从父项继承的规则的结构体,缓存是在上下文树上完成的(颜色属性实际上是继承的,但 Firefox 将其视为重置项,并将其缓存在规则树上)。

例如,如果我们在段落中添加字体规则:

p {font-family: Verdana; font size: 10px; font-weight: bold}

那么,段落元素是上下文树中 div 的子元素,就可以共享与其父元素相同的 font 结构体。这种情况是指没有为段落指定字体规则的情况。

在没有规则树的 WebKit 中,系统会遍历四次匹配的声明。首先应用不重要的高优先级属性(由于其他属性依赖于这些属性,因此应首先应用这些属性,例如展示广告),然后是高优先级重要规则、普通优先级不重要规则,最后是普通优先级重要规则。 这意味着多次出现的属性会根据正确的层叠顺序进行解析。最后的胜出者。

总而言之,共享样式对象(整个对象或其中的部分结构体)可以解决第 1 个和第 3 个问题。Firefox 规则树也有助于以正确的顺序应用属性。

操控规则以轻松匹配

样式规则有多个来源:

  1. 外部样式表或样式元素中的 CSS 规则。 css p {color: blue}
  2. 内嵌样式属性,例如 html <p style="color: blue" />
  3. HTML 可视化属性(映射到相关的样式规则) html <p bgcolor="blue" /> 后两个元素可以轻松与该元素匹配,因为他拥有样式属性,并且 HTML 属性可以使用该元素作为键映射。

如上文问题 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 元素寻找规则。类映射将包含一个“错误”“p.error”规则。 div 元素在 ID 表(键为 ID)和标记表中有相关规则。 剩下的工作就是查明通过键提取的哪些规则真正匹配。

例如,如果 div 的规则是:

table div {margin: 5px}

它仍会从标记映射中提取,因为键是最右侧的选择器,但它与我们的 div 元素不匹配,因为 div 没有表祖先实体。

WebKit 和 Firefox 都进行了这一处理。

样式表层叠顺序

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

如果定义不止一个,问题就会开始 - 解决问题的级联顺序如下。

样式属性的声明可能会出现在多个样式表中,也可能在一个样式表中出现多次。 这意味着应用规则的顺序非常重要。这称为“层叠式”订单。 根据 CSS2 规范,层叠顺序为(从低到高):

  1. 浏览器声明
  2. 用户普通声明
  3. 作者普通声明
  4. 作者重要声明
  5. 用户重要声明

浏览器声明是最不重要的,用户只有在该声明被标记为重要时才会替换网页作者的声明。 具有相同顺序的声明将按特异性排序,然后按指定顺序。 HTML 可视化属性会转换为匹配的 CSS 声明。它们被视为低优先级的作者规则。

明确性

选择器的特异性由 CSS2 规范定义,具体如下:

  1. 如果来自的声明是“样式”,则计为 1属性,而不是带有选择器的规则,否则为 0 (= a)
  2. 计算选择器中 ID 属性的数量 (= b)
  3. 计算选择器中其他属性和伪类的数量 (= c)
  4. 统计选择器中元素名称和伪元素的数量 (= 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 使用一个标记来标记是否已加载所有顶级样式表(包括 @imports)。 如果在附加时未完全加载样式,则使用占位符并在文档中进行标记,然后在样式表加载完毕后重新计算样式。

布局

呈现器在创建后添加到呈现树时,并不包含位置和大小信息。计算这些值的过程称为布局或自动重排。

HTML 使用基于流的布局模型,这意味着大多数情况下只需一次遍历就能计算出几何信息。稍后“在数据流中”的元素通常不会影响“流中”较早元素的几何形状,因此布局可以按从左到右、从上到下的顺序贯穿文档。不过,也有一些例外情况:例如,HTML 表格可能需要进行多次传递。

坐标系相对于根坐标系。使用向上和向左坐标。

布局是一个递归过程。它从根呈现器(对应于 HTML 文档的 <html> 元素)开始,布局以递归方式继续遍历部分或全部帧层次结构,为需要它的每个渲染程序计算几何信息。

根呈现器的位置为 (0,0),其尺寸为视口,即浏览器窗口的可见部分。

所有渲染程序都有一个“布局”或“重排”方法中,每个渲染程序都会调用其需要布局的子项的布局方法。

Dirty 位系统

为避免对所有小更改都执行完整的布局,浏览器使用“脏位”系统。 发生更改或添加了自身及其子项为“dirty”的渲染程序:需要布局。

有两种标记:“dirty”和“children are dirty”也就是说,尽管渲染程序本身没有问题,但它至少有一个子级需要布局。

全局布局和增量布局

布局可在整个渲染树上触发,即“全局”布局。 以下原因可能会导致此问题:

  1. 会影响所有渲染程序的全局样式更改,例如字体大小更改。
  2. 屏幕大小经过调整

布局可以是增量式的,只有 dirty 呈现器会进行布局(这可能会造成一些损害,导致需要额外的布局)。

当渲染程序为 dirty 时,会(异步)触发增量布局。例如,在来自网络的额外内容被添加到 DOM 树之后,新的渲染器被附加到渲染树中。

增量布局。
图 18:增量布局 - 仅布局了 dirty 渲染程序及其子级

异步布局和同步布局

增量布局是异步执行的。Firefox 将“reflow 命令”加入队列而调度程序会触发这些命令的批量执行。 WebKit 还有一个用于执行增量布局的计时器 - 遍历布局树并“dirty”开始布局

请求样式信息(如“offsetHeight”)的脚本可以同步触发增量布局。

全局布局通常会同步触发。

有时,系统会在初始布局之后以回调的形式触发布局,这是因为某些属性(如滚动位置)发生了变化。

优化

当布局由“调整大小”触发时或渲染器位置(而非大小)发生变化时,渲染尺寸会从缓存中获取,并且不会重新计算...

在某些情况下,只修改了一个子树,因此布局并非从根节点开始。当更改是局部更改而不影响其周围环境时,就会发生这种情况,比如将文本插入到文本字段中(否则,每次按键都会触发从根节点开始的布局)。

布局处理

布局通常具有以下模式:

  1. 父级渲染程序确定自己的宽度。
  2. 父级元素检查子级,并且:
    1. 放置子呈现器(设置其 x 和 y)。
    2. 根据需要调用子布局 - 它们是 dirty 的,或者我们位于全局布局中,或出于某些其他原因 - 这样会计算子元素的高度。
  3. 父级使用子级的累计高度以及边距和内边距的高度来设置自己的高度,父级渲染程序的父级将使用此高度。
  4. 将其 dirty 位设置为 false。

Firefox 使用“状态”对象(nsHTMLReflowState)作为布局(称为“reflow”)的参数。此外,状态包括父级宽度。

Firefox 布局的输出为“指标”对象(nsHTMLReflowMetrics)。它将包含渲染程序计算的高度。

宽度计算

渲染程序的宽度是根据容器块的宽度(渲染程序的样式“width”)计算得出的,属性,即外边距和边框。

例如,以下 div 的宽度:

<div style="width: 30%"/>

WebKit 会按以下方式计算(RenderBox 类 calcWidth 方法):

  • 容器的宽度取容器的 availableWidth 和 0 中的较大值。 在本例中, availableWidth 是 contentWidth,计算公式如下:
clientWidth() - paddingLeft() - paddingRight()

clientWidth 和 clientHeight 表示对象的内部 排除边框和滚动条。

  • 元素的宽度就是“width”style 属性。 通过计算占容器宽度的百分比,可以将其作为绝对值计算得出。

  • 现在,添加了水平边框和内边距。

现在计算得出的是“preferred width”。 现在,将计算最小和最大宽度。

如果首选宽度大于最大宽度,则使用最大宽度。 如果小于最小宽度(最小的不可破开单元),则使用最小宽度。

系统会缓存这些值以备需要布局时使用,但宽度不会变化。

换行

当处于布局中间的渲染程序确定需要中断时,该渲染程序会停止,并向布局的父级传播需要中断的情形。 父级会创建额外的渲染程序,并对其调用布局。

绘画

在绘制阶段,系统会遍历渲染树,并将渲染器的“paint()”方法在屏幕上显示内容。 绘制使用界面基础架构组件。

全局和增量

与布局一样,绘制也是全局(绘制整个树)或增量绘制。 在增量绘制中,部分呈现器发生了更改,但不会影响整个树。 更改后的渲染程序使其在屏幕上的矩形失效。 这会导致操作系统将其视为“脏区域”并生成“绘制”事件。 操作系统会非常巧妙地将多个区域合并为一个。 在 Chrome 中,情况要更复杂一些,因为渲染程序处于与主进程不同的进程中。Chrome 会在某种程度上模拟操作系统的行为。 展示层会监听这些事件,并将消息委托给呈现根节点。系统会遍历呈现树,直至找到相关的呈现器。它会对自身(通常及其子项)重绘。

绘制顺序

CSS2 定义了绘制流程的顺序。 这实际上就是元素在堆叠上下文中的堆叠顺序。这些堆栈会从后往前绘制,因此这个顺序会影响绘制。 块渲染程序的堆叠顺序为:

  1. 背景颜色
  2. 背景图片
  3. 边框
  4. 孩子
  5. Outline

Firefox 显示列表

Firefox 遍历呈现树,为绘制的矩形构建一个显示列表。 它包含与矩形相关的呈现器,并且按照正确的绘制顺序(先是呈现器的背景,然后是边框等)。

这样一来,重新绘制树只需遍历一次即可,而无需多次遍历 - 绘制所有背景,然后绘制所有图片,最后绘制所有边框等。

Firefox 对此过程进行了优化,不会添加隐藏的元素,例如完全位于其他不透明元素之下的元素。

WebKit 矩形存储

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

动态更改

在发生变更时,浏览器会尽量做出尽可能少的操作。 因此,元素的颜色改变只会导致该元素重新绘制。 元素位置的变化会导致该元素及其子元素(可能还有同级元素)进行布局和重绘。 添加 DOM 节点后,会对该节点进行布局和重绘。 重大更改,例如增加“html”的字体大小元素会导致缓存失效,以及整个树的重新布局和绘制。

呈现引擎的线程

呈现引擎采用单线程模式。除网络操作外,几乎所有操作都是在单线程中进行的。 在 Firefox 和 Safari 中,该线程就是浏览器的主线程。在 Chrome 中,该线程是标签页进程的主线程。

网络操作可由多个并行线程执行。并行连接数量是有限的(通常为 2 至 6 个)。

事件循环

浏览器的主线程是事件循环。 而是形成一个无限循环,让整个进程保持活跃状态。它会等待事件(如布局和绘制事件)并进行处理。 以下是 Firefox 的主事件循环代码:

while (!mExiting)
    NS_ProcessNextEvent(thread);

CSS2 可视化模型

画布

根据CSS2 规范, 术语“画布”指的是“呈现格式结构的空间”,即浏览器绘制内容的区域。

对于空间的每个尺寸,画布都是无限的,但浏览器会根据视口的尺寸选择初始宽度。

根据 www.w3.org/TR/CSS2/zindex.html 的说明, 如果画布包含在另一个画布内,则它是透明的;如果不是,则会采用浏览器定义的颜色。

CSS 框模型

CSS 框模型描述了为文档树中的元素生成的矩形框,并根据可视化格式模型进行布局。

每个框都有一个内容区域(例如文本、图片等)以及可选的周围内边距、边框和外边距区域。

CSS2 框模型
图 19:CSS2 框模型

每个节点会生成 0...n 个这样的框。

所有元素都有一个“显示屏”属性,它决定了生成的框的类型。

示例:

block: generates a block box.
inline: generates one or more inline boxes.
none: no box is generated.

默认值为 inline,但浏览器样式表可以设置其他默认值。 例如:“div”的默认显示位置。

您可以在以下网址找到默认样式表示例:www.w3.org/TR/CSS2/sample.html

定位方案

有三种方案:

  1. 正常:根据对象在文档中的位置进行定位。也就是说,它在渲染树中的位置就和它在 DOM 树中的位置一样,并根据其框类型和尺寸进行布局。
  2. 浮动:对象的布局方式与普通流动一样,然后尽可能向左或向右移动
  3. 绝对:对象在渲染树中与 DOM 树中的位置不同

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

  • 静态和相对的流都会引发正常流
  • 绝对定位和固定定位会导致进行绝对定位

static 定位未定义位置,而是使用默认定位。 在其他方案中,作者指定位置:top、bottom、left、right。

框的布局方式取决于:

  • 框类型
  • 框尺寸
  • 定位方案
  • 外部信息,例如图片大小和屏幕尺寸

框类型

Block box:形成一个 block,在浏览器窗口中有自己的矩形区域。

屏蔽框。
图 20:Block Box

inline 框:没有自己的代码块,但位于容器代码块内。

内嵌框。
图 21:inline 框

block 采用的是一个接一个的垂直格式, inline 采用水平格式。

Block 和 Inline 格式。
图 22:分块和内嵌格式

inline 框放置在行中或“行框”中。 这些行至少和最高的框一样高,但可以更高(当框按“基线”对齐时)- 表示元素的底部与底部以外的另一个框的位置对齐。 如果容器宽度不够,inline 元素就会分多行显示。 通常情况下,段落中会出现这种情况。

行。
图 23:线条

定位

相对

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

相对定位。
图 24:相对定位

浮点数

浮动框会移动到行的左侧或右侧。有趣的特性是,其他框在其周围流动。 HTML:

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

格式如下:

浮点型。
图 25:浮点

绝对值和固定值

布局是准确定义的,与普通流无关。元素不参与普通流。 尺寸是相对于容器而言的。 在固定格式中,容器即为视口。

固定位置。
图 26:固定定位

分层展示

这是通过 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>

结果将如下所示:

固定位置。
图 27:固定定位

虽然在标记中,红色 div 位于绿色 div 之前,并且在常规流程中之前已绘制过,但 Z-index 属性较高,因此它在根框的堆栈中更加靠前。

资源

  1. 浏览器架构

    1. 格罗斯库斯、艾伦。网络浏览器参考架构 (pdf)
    2. Gupta、Vineet。浏览器的工作原理 - 第 1 部分 - 架构
  2. 解析

    1. Aho, Sethi, Ullman, Compilers: Principles, Techniques, and Tools(也称为“Dragon Book”),Addison-Wesley,1986 年
    2. Rick Jelliffe。大胆而美丽:两种新的 HTML 5 草稿。
  3. Firefox

    1. L. David Baron,Fast HTML and CSS: Layout Engine Internals for Web Developers
    2. L. David Baron,Fast HTML and CSS: Layout Engine Internals for Web Developers(Google 技术讲座视频)
    3. L. David Baron,Mozilla 的布局引擎
    4. L. David Baron,Mozilla Style System 文档
    5. Chris Waterson,Notes on HTML Reflow
    6. Chris Waterson,Gecko 概览
    7. Alexander Larsson,The life of an HTML HTTP request
  4. WebKit

    1. David Hyatt,实施 CSS(第 1 部分)
    2. David Hyatt,WebCore 概览
    3. David Hyatt,WebCore 渲染
    4. David Hyatt,FOUC Problem
  5. W3C 规范

    1. HTML 4.01 规范
    2. W3C HTML5 规范
    3. 层叠样式表第 2 级修订版 1 (CSS 2.1) 规范
  6. 浏览器构建说明

    1. Firefox。https://developer.mozilla.org/Build_Documentation
    2. WebKit。http://webkit.org/building/build.html

翻译

此页已两次翻译为日语:

您可以查看外部托管的 韩语土耳其语

感谢大家!