从整个网页的加载和渲染过程来看,CSS解释器和规则匹配处于DOM树建立之后,RenderObject树(下一篇文章会介绍)建立之前,CSS解释器解释后的结果会保存起来,然后RenderObject树基于该结果进行规则匹配和布局计算。当网页有用户交互或者动画等动作的时候,通过CSSOM技术,JavaScript代码同样可以很方便的修改CSS样式,Webkit此时需要重新解释样式并重复以上这一过程。
一、CSSOM(CSS Object Model)
通常我们的CSS代码都是静态的,那么CSS有没有提供一些方法可以让开发者写一些脚本去操作它呢?这就是CSSOM,成为CSS对象模型。它的思想是在DOM中的一些节点接口中,加入获取和操作CSS属性或者接口的JavaScript接口。因而JavaScript可以动态操作CSS样式。
对于内部和外部样式表,CSSOM定义了样式表的接口,称为CSSStyleSheet,这是一个可以在JavaScript代码中访问的接口。借助于该接口,开发者可以在JavaScript中获取样式表的各种信息,例如CSS的href、样式表类型type、规则信息cssRules等,甚至可以获取样式表中的CSS规则列表。这个接口同DOM中的Script或者Link节点不一样,它是CSSOM定义的心接口。开发者可以通过document.stylesheets
查看当前网页中包含的所有CSS样式表,这是因为CSSOM对DOM中的Document接口进行了扩展。
W3C还定义了另一个规范,那就是CSSOM View,它的基本含义是增加一些新的属性到Window、Document、Element、HTMLElement和MouseEvent等接口,用于表示跟视图相关的特征,例如窗口大小、网页滚动位置、元素的位置、鼠标事件的坐标等信息。
二、CSS解释器和规则匹配
接下来看一下Webkit如何解释CSS代码并选择相应的规则。
1. 解释过程
CSS解释器是指从CSS字符串经过CSS解释器处理后变成渲染引擎的内部规则表示的过程。这一过程并不复杂,基本的思想是由CSSParser类负责。当Webkit需要解释CSS内容的时候,调用CSSParser来负责,最后Webkit将创建好的结构设置到StyleSheetContents对象中。
在解释网页中自定义的CSS样式之前,实际上Webkit渲染引擎会为每个网页设置一个默认的样式,这决定了网页所没有设置的元素属性及其属性默认值和将要显示的效果。一般来讲,不同的Webkit移植可以设置不同的默认样式。
2. 样式规则匹配
样式规则建立完成之后,Webkit保存规则结构在DocumentRuleSets对象中。当DOM的节点建立之后,Webkit会为其中的一些节点(可视节点)选择合适的样式信息。这些工作都是由StyleResolver负责。
基本的思路是使用StyleResolver来为DOM的元素节点匹配样式。StyleResolver类根据元素的信息,例如Tag Name、Class等,从样式规则中查找最匹配的规则,然后将样式信息保存到新建的RenderStyle中。最后这些RenderStyle对象被RenderObject使用。
样式的匹配则是由ElementRuleCollector来计算并获得,它根据元素的属性等信息,从之前的DocumentRuleSets中获取规则集合,依次按照ID、Class、Tag等选择器信息逐次匹配获得元素的样式。具体的过程是:
- 当Webkit需要为HTML元素创建RenderObject的时候,首先StyleResolver负责获取样式信息,并返回RenderStyle对象,RenderStyle对象包含了匹配完的结果样式信息;
- 根据实际需求,每个元素可能需要匹配不同来源的规则,依次是浏览器规则集合、用户规则集合和HTML网页中包含的自定义规则集合。这三个规则的匹配方式是类似的,这里以自定义规则匹配为例;
- 对于自定义规则集合,它先查找ID规则,检测有无匹配的规则,之后依次检测类型规则、标签规则等。如果某个规则匹配上该元素,Webkit把这些规则保存到匹配结果中;
- 最后,Webkit对这些规则进行排序。对于该元素需要的样式属性,Webkit选择从高优先级规则中选取,并将样式属性返回。
3. JavaScript设置样式
CSSOM定义了JavaScript访问样式的能力和方式。在Webkit中,这需要JavaScript引擎和渲染引擎共同来完成。之后的文章会详细介绍JavaScript引擎。
大致的过程是,JavaScript引擎调用设置属性值的公共处理函数,然后该函数调用属性值解析函数。而后Webkit将解析后的信息设置到元素的style属性的样式webkitTransform中,然后设置标记表明该元素需要重新计算样式,并触发重新布局。最后就是Webkit的重新绘制。
三、Webkit布局
1. 基础
当Webkit创建RenderObject对象之后,每个对象是不知道自己的位置、大小等信息的,Webkit根据盒模型来计算它们的位置大小信息的过程称为布局计算(排版)。
布局计算根据其计算的范围大致可以分为两类:
- 对整个RenderObject树进行的计算;
- 对RenderObject树中某个子树的计算,常见的是文本元素或者
overflow:auto;
,这种情况一般是其子树布局的改变不会影响其周围元素的布局,因而不需要重新计算更大范围内的布局。
2. 布局计算
布局计算是一个递归的过程,因为一个节点的大小通常需要先计算它的子女节点的位置、大小等信息。RenderObject节点计算布局的主要逻辑都是由RenderObject的layout函数来完成,大致过程如图:
- layout函数会判断RenderObject节点是否需要重新计算,通常这需要通过检查数组中相应的标记位、子女是否需要计算布局来确定;
- layout函数会确定网页的宽度和垂直方向上的外边距,这是因为网页通常是在垂直方向滚动,水平方向尽量不需要滚动;
- layout函数会遍历其每一个子女节点,依次计算它们的布局。每一个元素会实现自己的layout函数,根据特定的算法来计算该类型元素的布局。如果页面元素定义了自己的宽高,Webkit按照定义的宽高来确定元素的大小,而对于文本节点这种内联元素则需要结合其字号大小以及文字数量来确定其对应的宽高。如果页面元素所确定的宽高超过了布局容器所能提供的宽高,同时
overflow:visible
或者overflow:visible
,Webkit会提供滚动条来保证可以显示所有内容,一般来说页面元素的宽高是在布局的时候通过相关计算得出来的。如果元素由子女,则Webkit需要递归这一过程;- 节点根据它的子女们的大小计算得出自己的高度,整个过程结束。
哪些情况下需要重新计算布局呢?总体来讲,只要样式发生变化,Webkit都需要重新计算,有以下一些情况:
- 当网页首次被打开的时候,浏览器设置网页的可视区域(viewport),并调用计算布局的方法。就是说当可视区域发生变化的时候,Webkit需要重新计算布局;
- 网页的动画会触发布局计算。当网页显示结束之后,动画可能改变样式属性,Webkit就需要重新计算;
- JavaScript通过CSSOM直接修改样式信息,也会触发Webkit重新计算布局;
- 用户的交互也会触发布局计算,例如滚动网页。
CSS的布局计算是以包含块和盒模型为基础的,这表示这些元素的布局计算都依赖于块。但是,CSS标准也规定了行内元素,它们和块元素显示不太一样的是它们不会独占一行,而是在行内显示。为此,Webkit的处理方式是–对于一个块元素对应的RenderObject对象,它的子女要么都是块元素的RenderObject对象,要么都是非行内元素的RenderObject,这可以通过建立匿名块(Anonymous Blok)对象来实现,下一篇文章会介绍。
此外,布局计算是比较耗时间的,更糟糕的是,一旦布局发生变化,Webkit可能需要后面的重新绘制操作。