浏览器是如何工作的 上 解析与呈现

下篇传送门: 浏览器是如何工作的 下 布局与绘制

前言

本文主要翻译于Tali Garsiel在2009年10月的一篇介绍Webkit和Gecko内核的经典文章How browsers work。尽管在面试和工作上用不到这么细节,但是学习浏览器的内部原理将让我们可以更好地理解一些最优开发实践的道理。

浏览器

浏览器可以说是PC和移动设备上最常用的软件应用了。目前的主流浏览器有5个:IE,Firefox,Safari,Chrome和Opera。浏览器的主要功能是协助用户向服务器发起请求,并在窗口中展示请求的网络资源(HTML文档,或图片,或pdf文档,或其他可以被URI指定位置的类型)。

浏览器解释并展示HTML稳当的方式规定在HTML和CSS规范中。这些规范由W3C组织制订和维护。但是多年来,各浏览器并没有完全遵从这些规范(直到现在也是如此),从而带来兼容性的问题。

从高层来看,浏览器的用户界面(并没有规范去规定它该如何设计)是类似的,有用来输入URI的地址栏,前进和后退按钮,书签管理选项,刷新和停止刷新按钮,主页按钮等。这些最佳实践是自然发展和相互模仿的结果。它的背后由下面的一些组件组成:

  • 用户界面和界面后端,即UI
  • 浏览器引擎,负责在UI和浏览器各组件间传递指令
  • 渲染引擎,负责渲染请求的内容
  • 网络,底层的网络调用
  • JavaScript引擎
  • 数据存储,持久化存储浏览器的各种数据

值得注意的是,Chrome的每个标签页分别对应一个渲染引擎实例,且都是一个独立的进程。(知道为什么Chrome那么吃内存了么😂)

渲染引擎

渲染引擎的工作是渲染(文章原话),默认情况下,渲染引擎能呈现HTML、XML文档和图片。通过插件还可以展示其他类型内容(如PDF查看器插件显示PDF文档)。文章主要讨论了Firefox的Gecko引擎和Chrome的Webkit引擎。

渲染引擎的主要流程大致是获取并解析HTML文档构建DOM树,之后创建呈现树,呈现树包含有视觉属性(如颜色和尺寸),最后进入布局和绘制阶段。需要指出的是,这是个复杂和渐进的过程,为了更好地用户体验,渲染引擎通常会先将解析完成的部分HTML显示出来。下面是Webkit和Gecko的渲染引擎主流程。

Webkit

Gecko

Gecko和Webkit的术语略有不同,整体流程却是相似的。Gecko将视觉格式化元素组成的树称为“框架树”。每个元素都是一个框架。WebKit使用的术语是“呈现树”,它由“呈现对象”组成。对于元素的放置,WebKit 使用的术语是“布局”,而Gecko称之为“重排”。另外,Gecko在HTML和DOM树间还有一个称为“内容槽”的层用于生成DOM元素。

解析

关于解析HTML文档,原文做了详尽深入的讲解。由于侧重点不同,这里做些精炼。

解析文档是指将文档转化成为有意义的结构,也就是可让代码理解和使用的结构。解析得到的结果通常是代表了文档结构的节点树,它称作解析树或者语法树。

解析的过程通常分为词法分析语法分析。前者是指将内容拆解成合法标记的过程,由词法分析器完成;后者指应用语言的语法规则,由解析器完成。

词汇通常用正则表达式表示。例如,我们的示例语言可以定义如下:

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

正如您所看到的,这里用正则表达式给出了整数的定义。语法通常使用一种称为BNF的格式来定义。我们的示例语言可以定义如下:

1
2
3
expression :=  term  operation  term
operation := PLUS | MINUS
term := INTEGER | expression

之前我们说过,如果语言的语法是与上下文无关的语法,就可以由常规解析器进行解析。与上下文无关的语法的直观定义就是可以完全用BNF格式表达的语法。

解析器

有两种基本类型的解析器:自上而下解析器自下而上解析器。直观地来说,自上而下的解析器从语法的高层结构出发,尝试从中找到匹配的结构。而自下而上的解析器从低层规则出发,将输入内容逐步转化为语法规则,直至满足高层规则。

例如,我们要解析一个2 + 3 - 1的表达式,自上而下的解析器会从高层的规则开始:首先将2 + 3标识为一个表达式,然后将2 + 3 - 1标识为一个表达式。自下而上的解析器将扫描输入内容,找到匹配的规则后,将匹配的输入内容替换成规则。如此继续替换,直到输入内容的结尾。部分匹配的表达式保存在解析器的堆栈中。

有一些工具可以帮助您生成解析器,它们称为解析器生成器。您只要向其提供您所用语言的语法(词汇和语法规则),它就会生成相应的解析器。WebKit使用了两种非常有名的解析器生成器:用于创建词法分析器的Flex以及用于创建解析器的Bison。Flex的输入是包含标记的正则表达式定义的文件。Bison的输入是采用BNF格式的语言语法规则。

事情到了HTML这里变得麻烦了些。首先,HTML解析器的任务是将HTML标记解析成解析树。HTML词汇和语法在W3C的规范(目前版本是HTML5)中有着定义。但是HTML并不能很容易地用解析器所需的与上下文无关的语法来定义。HTML的正规格式DTD(Document Type Definition)并不是一种上下文无关的语法。

原因就是HTML并不是XML。HTML最初野蛮生长的日子里,实现方式不一而足,HTML的严格版变体XHTML并没有得到广泛的支持。为了保证兼容性,饶了诸多浏览器一命,包容许多并不合适的使用方式,简化网络开发。DTD中的严格模式下是完全遵守HTML规范的。

HTML解析

解析器的输出解析树是由DOM元素和属性节点构成的树结构。DOM是文档对象模型 (Document Object Model) 的缩写。它是HTML文档的对象表示,同时也是JavaScript与HTML元素之间的接口。解析树的根节点是Document对象。DOM与标记tag间几乎是一一对应的关系。

如上文所说,由于语言的宽容性和原内容的可更改性,HTML无法用常规的自上而下或自下而上的解析器来解析。根据原文的说法,HTML的解析算法标记化树构建组成。

标记化是词法分析过程,将输入内容解析成多个标记。HTML标记包括起始标记、结束标记、属性名称和属性值。标记生成器识别标记,传递给树构造器,然后接受下一个字符以识别下一个标记;如此反复直到输入的结束。

其中标记化算法的输出结果是HTML标记。该算法使用状态机来表示。每一个状态接收来自输入信息流的一个或多个字符,并根据这些字符更新下一个状态。当前的标记化状态和树结构状态会影响进入下一状态的决定。算法相当复杂,无法在此详述,下面给出一个简要的示例(来自原文)

1
2
3
4
5
<html>
<body>
Hello world
</body>
</html>

初始状态是数据状态。遇到字符<时状态更改为“标记打开状态”。接收一个a-z字符会创建“起始标记”,状态更改为“标记名称状态”。这个状态会一直保持到接收>字符。在此期间接收的每个字符都会附加到新的标记名称上。在本例中,我们创建的标记是html标记。

遇到>标记时,会发送当前的标记,状态改回“数据状态”。<body>标记也会进行同样的处理。目前htmlbody标记均已发出。现在我们回到“数据状态”。接收到 Hello world中的H字符时,将创建并发送字符标记,直到接收</body>中的<。我们将为Hello world中的每个字符都发送一个字符标记。

现在我们回到“标记打开状态”。接收下一个输入字符/时,会创建end tag token并改为“标记名称状态”。我们会再次保持这个状态,直到接收>。然后将发送新的标记,并回到“数据状态”。</html>输入也会进行同样的处理。

树构造器中运行着树构造算法。在创建解析器的同时,也会创建Document对象。在树构建阶段,以Document为根节点的DOM树也会不断进行修改,向其中添加各种元素。标记生成器发送的每个节点都会由树构建器进行处理。规范中定义了每个标记所对应的DOM元素,这些元素会在接收到相应的标记时创建。这些元素不仅会添加到DOM树中,还会添加到开放元素的堆栈中。此堆栈用于纠正嵌套错误和处理未关闭的标记。其算法类似标记化算法,也可以用状态机来描述。

我们同样以上面的代码为例,树构建阶段的输入是一个来自标记化阶段的标记序列。第一个模式是initial mode。接收HTML标记后转为before html模式,并在这个模式下重新处理此标记。这样会创建一个HTMLHtmlElement元素,并将其附加到Document根对象上。

然后状态将改为before head。此时我们接收body标记。即使我们的示例中没有head标记,系统也会隐式创建一个HTMLHeadElement,并将其添加到树中。

现在我们进入了in head模式,然后转入after head模式。系统对body标记进行重新处理,创建并插入HTMLBodyElement,同时模式转变为in body

现在,接收由“Hello world”字符串生成的一系列字符标记。接收第一个字符时会创建并插入Text节点,而其他字符也将附加到该节点。

接收body结束标记会触发after body模式。现在我们将接收HTML结束标记,然后进入after after body模式。接收到文件结束标记后,解析过程就此结束。

解析后与容错机制

在此阶段,浏览器会将文档标注为交互状态,并开始解析那些处于deferred模式的脚本,也就是那些应在文档解析完成后才执行的脚本。然后,文档状态将设置为“完成”,一个load事件随之触发。

我们在浏览HTML网页时从来不会看到“语法无效”的错误。这是因为浏览器会纠正任何无效内容,然后继续工作。不同浏览器的错误处理机制相当一致,但这种机制却并不是HTML当前规范的一部分。和书签管理以及前进/后退按钮一样,它也是浏览器在多年发展中的产物。很多网站都普遍存在着一些已知的无效HTML结构,每一种浏览器都会尝试通过和其他浏览器一样的方式来修复这些无效结构。

HTML5规范定义了一部分这样的要求。WebKit在HTML解析器类的开头注释中对此做了很好的概括。

解析器对标记化输入内容进行解析,以构建文档树。如果文档的格式正确,就直接进行解析。

遗憾的是,我们不得不处理很多格式错误的 HTML 文档,所以解析器必须具备一定的容错性。

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

  1. 明显不能在某些外部标记中添加的元素。在此情况下,我们应该关闭所有标记,直到出现禁止添加的元素,然后再加入该元素。
  2. 我们不能直接添加的元素。这很可能是网页作者忘记添加了其中的一些标记(或者其中的标记是可选的)。这些标签可能包括:HTML HEAD BODY TBODY TR TD LI(还有遗漏的吗?)。
  3. 向inline元素内添加block元素。关闭所有inline元素,直到出现下一个较高级的block元素。
  4. 如果这样仍然无效,可关闭所有元素,直到可以添加元素为止,或者忽略该标记。

错误的情况包括错误使用<br>,离散表格,过于复杂的标记层级结构。错误的html或body结束标记等。Webkit的具体代码展示略。

CSS解析

和HTML不同,CSS是上下文无关的语法,可以使用简介中描述的各种解析器进行解析。词法语法(词汇)是针对各个标记用正则表达式定义的:

1
2
3
4
5
6
7
comment   \/\*[^*]*\*+([^/*][^*]*\*+)*\/
num [0-9]+|[0-9]*"."[0-9]+
nonascii [\200-\377]
nmstart [_a-z]|{nonascii}|{escape}
nmchar [_a-z0-9-]|{nonascii}|{escape}
name {nmchar}+
ident {nmstart}{nmchar}*

其中ident表示标识符,如类名。name是元素ID。

语法则是采用 BNF 格式描述的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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*] ')' ]
;

如其中的ruleset表示,这个规则集是一个选择器,或者由逗号和空格(S表示空格)分隔的多个(数量可选)选择器。规则集包含了大括号,以及其中的一个或多个(数量可选)由分号分隔的声明。

Webkit使用Flex和Bison解析器生成器,通过 CSS 语法文件自动创建解析器。Firefox 使用的是人工编写的自上而下的解析器。这两种解析器都会将CSS文件解析成StyleSheet对象,且每个对象都包含CSS规则。

JS和CSS的处理

这也是经常的考点

脚本

解析器遇到<script>标记时会立即解析并执行脚本。文档的解析将停止,直到脚本执行完毕。如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续。此模型已经使用了多年,也在HTML4和HTML5规范中进行了指定。

脚本标注为defer时,它不会停止文档解析,而是等到解析结束才执行。HTML5中还增加了一个选项async,可将脚本标记为异步,在资源下载完毕后立即执行。

预解析

WebKit和Firefox都进行了这项优化。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。但是,预解析器不会修改DOM树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。

样式表

理论上来说,应用样式表不会更改DOM树,因此似乎没有必要等待样式表并停止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。如果当时还没有加载和解析样式,脚本就会获得错误的回复。

从而,Firefox在样式表加载和解析的过程中,会禁止所有脚本。而对于WebKit而言,仅当脚本尝试访问的样式属性可能受尚未加载的样式表影响时,它才会禁止该脚本。

呈现树

在DOM树构建的同时,浏览器还会构建另一个树结构:呈现树(Render Tree)。这是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是让您按照正确的顺序绘制内容。

Firefox将呈现树中的元素称为“框架”。WebKit使用的术语是呈现器或呈现对象。呈现器知道如何布局并将自身及其子元素绘制出来。

每个呈现器都表示一个矩形区域,通常对应于相关节点的CSS框,这一点在CSS2规范中有所描述。它包含诸如宽度、高度和位置等几何信息。框的类型会受到与节点相关的display样式属性的影响。下面是Webkit根据display属性的不同,针对同一个DOM节点创建不同呈现器的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;
}
和DOM树关系

呈现器和DOM元素是一一对应的,但是呈现树则不是。非可视化的DOM元素不会插入在呈现树中,例如head元素,或是display设为none的元素。同时,还有一些DOM元素对应着多个可视化对象,例如select元素。格式无效的HTML元素会根据CSS规范作出调整,如inline元素中同时包裹了block和inline元素(只能包含其中一种)。最后,有些呈现对象的位置和DOM节点位置不同,如浮动定位和绝对定位这样脱离文档流的元素。

构建过程

在Firefox中,展示层被注册为DOM更新的监听器。在监听到DOM改动后,展示层会把将框架创建工作委派给FrameConstructor,由其构造器解析样式并创建框架。

在WebKit中,解析样式和创建呈现器的过程称为attachment。每个DOM节点都有一个attach方法用于完成此项工作。attach是同步进行的,将节点插入DOM树需要调用新的节点attach方法。

处理htmlbody标记会构建呈现树根节点。这个根节点呈现对象对应于CSS规范中所说的容器block,这是最上层的 block,包含了其他所有block。它的尺寸就是视口,即浏览器窗口显示区域的尺寸。Firefox称之为ViewPortFrame,而WebKit称之为RenderView。这就是文档所指向的呈现对象。呈现树的其余部分以DOM树节点插入的形式来构建。

W3C的CSS2.1规范中有提到解析模型的问题。

样式计算

构建呈现树时,需要计算每个呈现对象的可视化属性。这是通过每个元素的样式来完成的。样式包括来自各种来源的样式表、inline样式元素(即style属性)和HTML中的可视化属性(如bgColor, width)。样式表的来源包括浏览器的默认样式表、由网页作者提供的样式表以及由浏览器用户提供的用户样式表等。

样式计算存在着许多困难:1)样式数据庞大,2)为元素查找匹配规则的过程复杂,3)CSS的层叠规则复杂。针对这些问题,firefox和Webkit有不同的处理方法。

Webkit会引用样式对象(RenderStyle)。这些对象在某些情况下可被不同同级节点共享,这些节点还有下面的要求:

  • 鼠标状态相同,如都是:hover
  • 没有元素ID
  • tag名应匹配
  • class属性应匹配
  • 链接状态(如:active)和焦点状态(如:focus)相同
  • 映射属性的集合完全相同
  • 不应被属性选择器匹配
  • 不能有任何inline样式属性
  • 不能使用同级选择器

Firefox为简化运算,使用了另外两种树,规则树样式上下文树,如下图所示。Webkit则通过DOM节点指向样式对象来实现。样式上下文包含端值。要计算出这些值,应按照正确顺序应用所有的匹配规则,并将其从逻辑值转化为具体的值。例如,如果逻辑值是屏幕大小的百分比,则需要换算成绝对的单位。

规则树

规则树的设计将所有匹配规则都存储在树中,它包含了所有匹配规则。路径的路径中的底层节点拥有较高的优先级。规则树包含了所有已知规则匹配的路径。规则的存储是延迟进行的。规则树不会在开始的时候就为所有的节点进行计算,而是只有当某个节点样式需要进行计算时,才会向规则树添加计算的路径。

原文从两个角度分析了规则树如何减少工作量:结构划分通过规则树计算样式上下文

样式上下文可分割成多个结构。这些结构体包含了特定类别(如bordercolor)的样式信息。结构中的属性都是继承的或非继承的。继承属性如果未由元素定义,则继承自其父代。非继承属性(也称为“重置”属性)如果未进行定义,则使用默认值。

规则树通过缓存整个结构(包含计算出的端值)为我们提供帮助。这一想法假定底层节点没有提供结构的定义,则可使用上层节点中的缓存结构。

在计算某个特定元素的样式上下文时,我们首先计算规则树中的一条对应路径,或者使用一条现有的路径。然后我们沿此路径应用规则,在这个样式上下文中填充结构。我们从路径中拥有最高优先级的底层节点(通常也是最特殊的选择器)开始,并向上遍历规则树,直到结构填充完毕。

如果该规则节点对于此结构没有任何规范,那么我们可以实现更好的优化:寻找路径更上层的节点,找到后指定完整的规范并指向相关节点即可。这是最好的优化方法,因为整个结构都能共享。这可以减少端值的计算量并节约内存。

如果我们找到了部分定义,就会向上遍历规则树,直到结构填充完毕。如果我们找不到结构的任何定义,那么假如该结构是继承类型,我们会在上下文树中指向父代的结构,这样也可以共享结构。如果是reset类型的结构,则会使用默认值。

如果最特殊的节点确实添加了值,那么我们需要另外进行一些计算,以便将这些值转化成实际值。然后我们将结果缓存在树节点中,供子代使用。如果某个元素与其同级元素都指向同一个树节点,那么它们就可以共享整个样式上下文

下面用一个例子来讲解上面晦涩的说明:

1
2
3
4
5
6
7
8
9
10
11
12
<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
2
3
4
5
6
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 结构包含四条边。 形成的规则树如下图所示

假设我们解析 HTML 时遇到了第二个<div>标记,我们需要为此节点创建样式上下文,并填充其样式结构。经过规则匹配,我们发现该<div>的匹配规则是第1、2和6条。这意味着规则树中已有一条路径可供我们的元素使用,我们只需要再为其添加一个节点以匹配第6条规则(规则树中的F节点)。我们将创建样式上下文并将其放入上下文树中。新的样式上下文将指向规则树中的F节点。

现在我们需要填充样式结构。首先要填充的是margin结构。由于最后的规则节点(F)并没有添加到margin结构,我们需要上溯规则树,直至找到在先前节点插入中计算过的缓存结构,然后使用该结构。我们会在指定margin规则的最上层节点(即B节点)上找到该结构。

我们已经有了color结构的定义,因此不能使用缓存的结构。由于color有一个属性,我们无需上溯规则树以填充其他属性。我们将计算端值(将字符串转化为RGB等)并在此节点上缓存经过计算的结构。

第二个<span>元素处理起来更加简单。我们将匹配规则,最终发现它和之前的span一样指向规则G。由于我们找到了指向同一节点的同级,就可以共享整个样式上下文了,只需指向之span的上下文即可。

处理规则简化匹配

样式规则来源于外部样式表、inline样式属性、HTML可视化属性。后两者很容易匹配。CSS规则可能会棘手,可以对它进行一些处理,便于访问。

样式表解析完毕后,系统会根据选择器将CSS规则添加到某个哈希表中。这些哈希表的选择器各不相同,包括ID、类名称、标记名称等,还有一种通用哈希表,适合不属于上述类别的规则。如果选择器是ID,规则就会添加到ID表中;如果选择器是类,规则就会添加到类表中,依此类推。这种处理可以大大简化规则匹配。我们无需查看每一条声明,只要从哈希表中提取元素的相关规则即可。以下面的CSS为例

1
2
3
p.error {color:red}
#messageDiv {height:50px}
div {margin:5px}

第一条规则将插入类表,第二条将插入ID表,而第三条将插入标记表。对于下面的HTML代码段:

1
2
<p class="error">an error occurred </p>
<div id="messageDiv">this is a message</div>

我们首先会为p元素寻找匹配的规则。类表中有一个error键,在下面可以找到p.error的规则。div元素在ID表(键为 ID)和标记表中有相关的规则。剩下的工作就是找出哪些根据键提取的规则是真正匹配的了。

正确的层叠顺序

样式对象具有与可视化属性一一对应的属性(均为CSS属性但更为通用)。如果某个属性未由任何匹配规则所定义,那么部分属性可由父代元素样式对象继承。其他属性具有默认值。不过如果定义不止一个,就会出现麻烦,这时需要通过层叠顺序来解决。

某个样式属性的声明可能会出现在多个样式表中,也可能在同一个样式表中出现多次。层叠顺序的重要性正体现在这里。根据CSS2规范,层叠的顺序为(优先级从低到高):

  1. Browser declarations
  2. User normal declarations
  3. Author normal declarations
  4. Author important declarations
  5. User important declarations

同样顺序的声明则会根据特异性(specity)进行排序,然后再是其指定顺序。HTML可视化属性会转换成匹配的CSS声明。它们被视为低优先级的Author normal declaration。

那么特异性是什么意思呢?根据CSS3 selectors specificity中的定义(和CSS2.1几乎一样),一个选择器的优先级计算如下:

  1. 如果声明来自于style属性,而不是带有选择器的规则,则记为1,否则记为0 (= a)
  2. 记为选择器中ID属性的个数 (= b)
  3. 记为选择器中其他属性和伪类的个数 (= c)
  4. 记为选择器中元素名称和伪元素的个数 (= d)

将四个数字按a-b-c-d这样连接起来(位于大数进制的数字系统中),构成特异性。所使用的进制取决于上述类别中的最高计数。例如,如果a=14,可以使用十六进制。如果a=17,那么需要使用十七进制;不过在正常使用中,几乎不会使用到如此深嵌套层级的选择器。规范中给出的例子很好地演示了这种系统的工作方式。

1
2
3
4
5
6
7
8
9
*             {}  /* 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 */

找到匹配的规则之后,应根据级联顺序将其排序。WebKit对于较小的列表会使用冒泡排序,对较大的列表使用归并排序。