渲染树构建

构建对象模型 中,我们根据 HTML 和 CSS 输入构建了 DOM 树和 CSSOM 树。 不过,它们都是独立的对象,分别网罗文档不同方面的信息:一个描述内容,另一个则是描述需要对文档应用的样式规则。

DOM 树和 CSSOM 树将合并后在浏览器屏幕上渲染像素

  • DOM 树与 CSSOM 树合并后形成渲染树
  • 渲染树只包含渲染网页所需的节点
  • 布局 计算每个对象的精确位置和大小
  • 最后一步是 绘制,使用最终渲染树将像素渲染到屏幕上

构建流程

浏览器将 DOM 和 CSSOM 合并成一个 渲染树,它会网罗网页上所有可见的 DOM 内容,以及每个节点的所有 CSSOM 样式信息。

渲染树构建

为了构建渲染树,浏览器大体上完成了下列工作:

  1. 从 DOM 树的根节点开始遍历每个可见节点
    • 某些节点不可见(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。
    • 某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略,例如,样式设置为 display: none 属性的节点。
  2. 对于每个可见节点,为其找到适配的 CSSOM 规则并应用它们。
  3. 发射可见节点,连同其内容和计算的样式。

遍历文档树

渲染对象是和 DOM 元素相对应,但这种对应关系并非一一对应。

非可视化元素

非可视化的 DOM 元素不会被插入渲染树中。

例如, <head> 标签以及里面的内容,以及 display:none 的元素也会被去除,但是 visibility 属性值为 hidden 的元素仍会显示。

复杂结构元素

部分 DOM 元素可对应多个可视化对象。它们往往是具有复杂结构的元素,无法用单一的矩形来描述。

例如, select 元素有 3 个渲染器:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮。如果由于宽度不够,文本无法在一行中显示而分为多行,那么新的行也会作为新的呈现器而添加。

另一个关于多渲染器的例子是格式无效的 HTML。根据 CSS 规范,行内元素只能仅包含块状元素或行内元素中的一种。如果出现了混合内容,则应创建匿名的块状渲染对象,以包裹行内元素。所以我们平时的 inline-block 可以设置宽高。

脱离文档流

部分渲染对象对应于 DOM 节点,但树中所在的位置与 DOM 节点不同。

例如,浮动定位和绝对定位的元素处于正常的文本流之外,在两棵树上的位置不同,渲染树上标识出真实的结构,并用一个占位结构标识出它们原来的位置。

渲染树及对应DOM树

样式计算

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

样式包括来自各种来源的样式表行内样式元素HTML 中的可视化属性。其中后者经过转化以匹配 CSS 样式属性。

样式表的来源包括浏览器的默认样式表由网页作者提供的样式表以及由浏览器用户提供的用户样式表(浏览器允许您定义自己喜欢的样式。以 Firefox 为例,用户可以将自己喜欢的样式表放在 Firefox Profile 文件夹下)。

样式计算存在以下难点:

  1. 样式数据是一个超大的结构,存储了无数的样式属性,这可能造成内存问题。
  2. 如果不进行优化,为每一个元素查找匹配的规则会造成性能问题。要为每一个元素遍历整个规则列表来寻找匹配规则,这是一项浩大的工程。选择器会具有很复杂的结构,这就会导致某个匹配过程一开始看起来很可能是正确的,但最终发现其实是徒劳的,必须尝试其他匹配路径。
  3. 应用规则涉及到相当复杂的层叠规则(用于定义这些规则的层次)

主线程解析CSS以添加计算后样式

共享样式数据

WebKit 节点会引用样式对象 (RenderStyle)。这些对象在某些情况下可以由不同节点共享。这些节点是同级关系,并且:

  1. 这些元素必须处于相同的鼠标状态(例如,不允许其中一个是 :hover 状态,而另一个不是)
  2. 任何元素都没有 id
  3. 标记名称应匹配
  4. 类属性应匹配
  5. 映射属性的集合必须是完全相同的
  6. 链接状态必须匹配
  7. 焦点状态必须匹配
  8. 任何元素都不应受属性选择器的影响,这里所说的“影响”是指在选择器中的任何位置有任何使用了属性选择器的选择器匹配
  9. 元素中不能有任何 inline 样式属性
  10. 不能使用任何同级选择器。WebCore 在遇到任何同级选择器时,只会引发一个全局开关,并停用整个文档的样式共享(如果存在)。这包括 + 选择器以及 :first-child:last-child 等选择器。

对规则进行处理以简化匹配

样式规则来源:

  • 外部样式表获样式元素中的 CSS 规则
p {
color: blue;
}
  • 行内样式属性及类似内容
<p style="color: blue" />
  • HTML 可视化属性(映射到相关的样式规则)
<p bgcolor="blue" />

后两种很容易和元素进行匹配,因为元素拥有样式属性,而且 HTML 属性可以使用元素作为键值进行映射。

样式表解析完毕后,系统会根据选择器将 CSS 规则添加到某个哈希表中。这些哈希表的选择器各不相同,包括 ID、类名称、标记名称等,还有一种通用哈希表,适合不属于上述类别的规则。如果是 ID 选择器,规则就会添加到 ID 表中;如果是类选择器,规则就会添加到类表中,以此类推。

这种处理可以大大简化规则匹配。我们无需查看每一条声明,只要从哈希表中提取元素的相关规则即可。这种优化方法可排除掉 95% 以上规则,因此在匹配过程中根本就不用考虑这些规则。

我们以如下的样式规则为例:

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

第一条规则将插入类表,第二条将插入 ID 表,而第三条将插入标记表。

<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. 浏览器声明
  2. 用户普通声明
  3. 作者普通声明
  4. 作者的 important 声明
  5. 用户的 important 声明

浏览器声明是重要程度最低的,而用户只有将该声明标记为 important 时才会覆盖网页作者的声明。同等级别的声明将根据 特异性 以及它们被定义时的顺序进行排序。HTML 可视化属性将被转换为匹配的 CSS 声明,它们被视为最低优先级的作者规则。

特异性

选择器的特异性由 CSS2 规范定义:

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

将四个数字按 a-b-c-d 这样连接起来(位于大数进位的数字系统中),构成特异性。

您使用的进取制取决于上述类别中的最高计数。

例如,如果 a = 14 ,您可以使用十六进制。如果 a = 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 */

参考资料