样式系统概述

Quantum CSS (Stylo)

从 Firefox 57 及更高版本开始,Gecko 使用了来自 Servo 的用 Rust 编写的并行样式系统。有一篇 概述,其中包含图形来帮助解释正在发生的事情。Servo wiki 有一些更详细的信息。

Gecko

样式部分的其余部分描述了 Firefox 56 及更早版本中使用的 Gecko 样式系统。某些部分可能仍然适用,但可能需要修改。

为了显示内容,Gecko 需要计算与每个 DOM 节点相关的样式。它是根据 CSS 规范中描述的模型来执行此操作的:此模型适用于 CSS 中指定的样式(例如,通过 'style' 元素、'xml-stylesheet' 处理指令或 'style' 属性)、表示属性指定的样式以及我们自己的用户代理样式表指定的默认样式。样式系统中有两组主要的数据结构

  • 首先,表示样式数据源的数据结构,例如 CSS 样式表或来自样式 HTML 属性的数据

  • 其次,表示给定 DOM 节点的计算样式的数据结构。

这些数据集结构大多是不同的(例如,它们以不同的方式存储值)。

从网络加载 CSS 样式表由 CSS 加载器 管理;然后,它们由 CSS 扫描器 进行标记化,并由 CSS 解析器 进行解析。那些附加到文档的样式表还向脚本公开 API,这些 API 被称为 CSS 对象模型或 CSSOM。

应用于文档的样式表由一个名为 样式集 的类管理。样式集通过两个接口与不同类型的样式表(表示 CSS 样式表、表示属性和 'style' 属性)交互:nsIStyleSheet 用于样式表的基本管理,以及 nsIStyleRuleProcessor 用于获取其中的样式数据。通常同一个对象实现这两个接口,除了最重要的案例,即 CSS 样式表,其中每个 CSS 级联的来源(用户/UA/作者)都有一个用于所有 CSS 样式表的单个规则处理器。

元素/框架的计算样式数据通过 mozilla::ComputedStyle 类(以前称为 nsStyleContext)公开给 Gecko 的其余部分。它没有为每个 CSS 属性都设置一个成员变量,而是将属性分解成称为样式结构的相关属性组。这些样式结构遵循这样的规则:单个结构中的所有属性要么默认继承(CSS 规范在属性定义中称为“继承:是”;我们称这些为继承结构),要么都不默认继承(我们称这些为重置结构)。以这种方式分离属性提高了在类似 ComputedStyle 对象之间共享结构并减少存储样式数据所需的内存量。ComputedStyle API 公开了获取每个结构的方法,因此您会看到类似 sc->GetStyleText()->mTextAlign 的代码来获取 text-align CSS 属性的值。(框架(参见下面的布局部分)也具有相同的 GetStyle* 方法,这些方法只是将调用转发到框架的 ComputedStyle。)

ComputedStyles 形成一个树状结构,其形状有点像内容树(除了我们将相同的兄弟 ComputedStyles 合并而不是保留两个;如果父级已合并,则此操作可以递归应用并合并堂兄弟等;我们不合并父/子 ComputedStyles)。ComputedStyle 的父级具有 ComputedStyle 在发生 CSS 继承时从中继承的样式数据。这意味着 DOM 元素的 ComputedStyle 的父级通常是该 DOM 元素父级的 ComputedStyle,因为这就是 CSS 继承的工作方式。

将样式表转换为计算样式数据的过程分为三个主要步骤,前两个步骤与 nsIStyleRule 接口密切相关,该接口表示样式数据的不可变源,在概念上表示(对于 CSS 样式规则,直接存储)一组属性:值对。(它类似于 CSS 样式规则的概念,除了它是不可变的;这种不变性允许进行重大优化。当通过脚本更改 CSS 样式规则时,我们创建一个新的样式规则。)

从样式表到计算样式数据的第一个步骤是查找应用于元素的样式规则的有序序列。该顺序表示哪些规则覆盖哪些其他规则:如果两个规则对同一属性具有值,则排名较高的规则获胜。(请注意,与 CSS 样式规则还有另一个区别:带有 !important 的声明使用单独的样式规则表示。)这是通过调用 nsIStyleRuleProcessor::RulesMatching 方法之一来完成的。有序序列存储在一个称为规则树的 trie 中:从规则树的根到规则树中任何(叶子或非叶子)节点的路径表示一系列规则,其中排名最高的规则距离根最远。每个规则节点(根除外)都指向一个规则,但由于一个规则可能出现在许多序列中,因此有时会有许多规则节点指向同一个规则。一旦我们有了这个列表,我们就创建一个 ComputedStyle(或找到一个合适的现有兄弟),并设置正确的父指针(用于继承)和规则节点指针(用于规则列表),以及其他一些信息(如伪元素)。

从样式表到计算样式数据的第二个步骤是从规则中获取获胜的属性:值对。(这仅为某些属性提供属性:值对;其余属性将根据属性是否默认继承而回退到继承或其初始值。)我们对每个样式结构执行此步骤(以及第三个步骤),这是第一次需要它的时候。这在 nsRuleNode::WalkRuleTree 中完成,我们要求每个样式规则通过调用其 MapRuleInfoInto 函数来填写其属性:值对。当被调用时,规则仅填写尚未填写的那些对,因为我们是从最高优先级规则到最低优先级规则进行调用的(因为在许多情况下,这允许我们在遍历整个列表之前停止,或者进行仅添加到规则树中较高缓存数据的局部计算)。

从样式表到计算样式数据的第三个步骤(各种缓存优化允许我们在许多情况下跳过此步骤)实际上是进行计算;这通常意味着我们将样式数据转换为 CSS 规范中属性定义中的“计算值”行中描述的数据类型。此转换发生在名为 nsRuleNode::Compute*Data 的函数中,其中中间的 * 表示样式结构的名称。这是样式表值存储格式到计算值存储格式转换发生的地方。

一旦我们有了计算样式数据,我们就会将其存储:如果计算样式数据中的样式结构不依赖于继承的值或来自其他样式结构的数据,那么我们就可以将其缓存在规则树中(然后重复使用它,无需重新计算,用于指向该规则节点的任何 ComputedStyles)。否则,我们将其存储在 ComputedStyle 上(在这种情况下,它可能会与 ComputedStyle 的后代 ComputedStyles 共享)。这就是将继承和非继承属性分开很有用的地方:在属性指定相对较少的常见情况下,我们通常可以在规则树中缓存非继承结构,并且我们通常可以在 ComputedStyle 树的上方和下方共享继承结构。

样式表结构中的所有权模型是引用计数结构(用于脚本可访问的事物)和直接拥有结构的混合。ComputedStyles 是引用计数的,并拥有它们的父级(它们从中继承),规则节点使用简单的标记和扫描收集器进行垃圾回收(通常不需要运行)。