作为前端,我们每天都在与CSS打交道,那么CSS的原理是什么呢?
开篇,我们还是不厌其烦的回顾一下浏览器的渲染过程,先上图:
浏览器渲染
正如上图所展示的,我们浏览器渲染过程分为了两条主线:
其一,HTML Parser 生成的 DOM 树; 其二,CSS Parser 生成的 Style Rules ;
在这之后,DOM 树与 Style Rules 会生成一个新的对象,也就是我们常说的 Render Tree 渲染树,结合 Layout 绘制在屏幕上,从而展现出来。
本文的重点也就集中在第二条分支上,我们来探究一下 CSS 解析原理。
浏览器 CSS 模块负责 CSS 脚本解析,并为每个 Element 计算出样式。CSS 模块虽小,但是计算量大,设计不好往往成为浏览器性能的瓶颈。
CSS 模块在实现上有几个特点:CSS 对象众多(颗粒小而多),计算频繁(为每个 Element 计算样式)。这些特性决定了 webkit 在实现 CSS 引擎上采取的设计,算法。如何高效的计算样式是浏览器内核的重点也是难点。
先来看一张图:
Webkit CSS 解析器
Webkit 使用 Flex 和 Bison 解析生成器从 CSS 语法文件中自动生成解析器。
它们都是将每个 CSS 文件解析为样式表对象,每个对象包含 CSS 规则,CSS 规则对象包含选择器和声明对象,以及其他一些符合 CSS 语法的对象,下图可能会比较明了:
Declaration
Webkit 使用了自动代码生成工具生成了相应的代码,也就是说词法分析
和语法分析
这部分代码是自动生成的,而 Webkit 中实现的 CallBack 函数就是在 CSSParser 中。
CSS 的一些解析功能的入口也在此处,它们会调用 lex , parse 等生成代码。相对的,生成代码中需要的 CallBack 也需要在这里实现。
举例来说,现在我们来看其中一个回调函数的实现,createStyleRule(),该函数将在一般性的规则需要被建立的时候调用,代码如下:
CSSRule* CSSParser::createStyleRule(CSSSelector* selector)
{
CSSStyleRule* rule = 0;
if (selector) {
rule = new CSSStyleRule(styleElement);
m_parsedStyleObjects.append(rule);
rule->setSelector(sinkFloatingSelector(selector));
rule->setDeclaration(new CSSMutableStyleDeclaration(rule, parsedProperties, numParsedProperties));
}
clearProperties();
return rule;
}
从该函数的实现可以很清楚的看到,解析器达到某条件需要创建一个 CSSStyleRule 的时候将调用该函数,该函数的功能是创建一个 CSSStyleRule ,并将其添加已解析的样式对象列表 m_parsedStyleObjects
中去,这里的对象就是指的 Rule 。
那么如此一来,经过这样一番解析后,作为输入的样式表中的所有 Style Rule 将被转化为 Webkit 的内部模型对象 CSSStyleRule 对象,存储在 m_parsedStyleObjects
中,它是一个 Vector
。
但是我们解析所要的结果是什么?
可能很多同学都知道排版引擎解析 CSS 选择器时是从右往左
解析,这是为什么呢?
对于上述描述,我们先有个大概的认知。接下来我们来看这样一个例子,参考地址
<div>
<div class="jartto">
<p>span> 111 span><p>
<p>span> 222 span><p>
<p><span> 333 <span><p>
<p><span class='yellow'> 444 <span><p>
<div>
<div>
CSS 选择器:
div > div.jartto p span.yellow{
color:yellow;
}
对于上述例子,如果按从左到右的方式进行查找:
这样的搜索过程对于一个只是匹配很少节点的选择器来说,效率是极低的,因为我们花费了大量的时间在回溯匹配不符合规则的节点。
如果换个思路,我们一开始过滤出跟目标节点最符合的集合出来,再在这个集合进行搜索,大大降低了搜索空间。来看看从右到左来解析选择器:
结果显而易见了,众所周知,在 DOM 树中一个元素可能有若干子元素,如果每一个都去判断一下显然性能太差。而一个子元素只有一个父元素,所以找起来非常方便。
试想一下,如果采用从左至右
的方式读取 CSS 规则,那么大多数规则读到最后(最右)才会发现是不匹配的,这样会做费时耗能,最后有很多都是无用的;而如果采取从右向左
的方式,那么只要发现最右边选择器不匹配,就可以直接舍弃了,避免了许多无效匹配。
浏览器 CSS 匹配核心算法的规则是以从右向左
方式匹配节点的。这样做是为了减少无效匹配次数,从而匹配快、性能更优。
CSS 样式表解析过程中讲解的很细致,这里我们只看 CSS 语法解释器,大致过程如下:
通过上文的了解,我们知道,当 CSS Parser 解析完 CSS 脚本后,会生成 CSSStyleSheetList ,他保存在Document 对象上。为了更快的计算样式,必须对这些 CSSStyleSheetList 进行重新组织。
计算样式就是从 CSSStyleSheetList 中找出所有匹配相应元素的 property-value 对。匹配会通过CSSSelector 来验证,同时需要满足层叠规则。
将所有的 declaration 中的 property 组织成一个大的数组。数组中的每一项纪录了这个 property 的selector,property 的值,权重(层叠规则)。
可能类似如下的表现:
p > a {
color : red;
background-color:black;
}
a {
color : yellow
}
div {
margin : 1px;
}
重新组织之后的数组数据为(weight我只是表示了他们之间的相对大小,并非实际值。)
选择器 | 声明 | 权重 |
---|---|---|
a | color:yellow; | 1 |
p > a | color:red; | 2 |
p > a | background-color:black; | 2 |
div | margin:1px; | 3 |
好了,到这里,我们来解决上述问题:
到这里就不难理解了,对浏览器来说,內联样式与其他的加载样式方式唯一的区别就是权重不同。
深入了解,请阅读Webkit CSS引擎分析
到这里,你以为完了?Too young too simple, sometimes naive!
浏览器还有一个非常棒的策略,在特定情况下,浏览器会共享 computedStyle,网页中能共享的标签非常多,所以能极大的提升执行效率!如果能共享,那就不需要执行匹配算法了,执行效率自然非常高。
也就是说:如果两个或多个 element 的 computedStyle 不通过计算可以确认他们相等,那么这些 computedStyle 相等的 elements 只会计算一次样式,其余的仅仅共享该 computedStyle 。
那么有哪些规则会共享 computedStyle 呢?
span>p style="color:red">paragraph1span>p>
span>p style="color:red">paragraph2span>p>
当然,知道了共享 computedStyle 的规则,那么反面我们也就了解了:不会共享 computedStyle 的规则,这里就不展开讨论了。
深入了解,请参考:Webkit CSS 引擎分析 - 高效执行的 CSS 脚本
CSS 选择器组合
如上图,我们可以看到不同的 CSS 选择器的组合,解析速度也会受到不同的影响,你还会轻视 CSS 解析原理吗?
感兴趣的同学可以参考:speed/validity selectors test for frameworks
Bad
p#id1 {color:red;}
Good
#id1 {color:red;}
当然,你非要这么写也没有什么问题,但这会增加 CSS 编译与解析时间,实在是不值当。
Bad
div > div > div > p {color:red;}
Good
p-class{color:red;}
Bad
p[id="id1"]{color:red;}
p[class="class1"]{color:red;}
Good
#id1{color:red;}
.class1{color:red;}