探究css解析原理
说明
尝试回答几个问题:
- 浏览器的渲染原理。
- Style Rules是怎么生成的?
- 怎么解析一个css选择器?
- 如果style标签写在了body区域,webkit在解析完这个style标签如何做呢?style标签写在了body区域好吗?
浏览器渲染页面
浏览器的渲染过程,如下图:
- HTML Parser解析html模板生成dom树;
- CSS Parser解析css生成Style Rules;
- dom树与Style Rules生成一个新的对象,也就是Render Tree渲染树,结合Layout绘制在屏幕上。
Webkit css解析器
css模块
浏览器 CSS 模块负责 CSS 脚本解析,并为每个 Element 计算出样式。CSS 模块在实现上有几个特点:CSS 对象众多(颗粒小而多),计算频繁(为每个 Element 计算样式)。
css规则对象
Webkit 使用 Flex 和 Bison 解析生成器从 CSS 语法文件中自动生成解析器,将每个 CSS 文件解析为样式表对象,每个对象包含 CSS 规则,CSS 规则对象包含选择器和声明对象,以及其他一些符合 CSS 语法的对象。
创建一个 CSSStyleRule 时会调用createStyleRule(),将其添加已解析的样式对象列表 m_parsedStyleObjects 中去。经过一番解析后,样式表中的所有 Style Rule 将被转化为 Webkit 的内部模型对象 CSSStyleRule 对象,存储在 m_parsedStyleObjects 中,它是一个 Vector。
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;
}
解析的目的
- 通过调用 CSSStyleSheet 的 parseString 函数,将上述 CSS 解析过程启动,解析完一遍后,把 Rule 都存储在对应的 CSSStyleSheet 对象中;
- 由于目前规则依然是不易于处理的,还需要将之转换成 CSSRuleSet。也就是将所有的纯样式规则存储在对应的集合当中,这种集合的抽象就是 CSSRuleSet;
- CSSRuleSet 提供了一个 addRulesFromSheet 方法,能将 CSSStyleSheet 中的 rule 转换为 CSSRuleSet 中的 rule ;
- 基于这些个 CSSRuleSet 来决定每个页面中的元素的样式;
css选择器的解析顺序
首先需要知道一个知识点,排版引擎解析css选择器时是从右往左解析的。
HTML经过解析会生成dom tree;而css在解析完毕后,需要将解析结果与dom tree的内容一起进行分析生成render tree,最终用来绘图。render tree中的元素被称为渲染节点(Webkit中为[renderers],Firefox中为[frames]),渲染节点与dom元素相对应,但又不是一一对应,一个dom元素可能有多个渲染节点(例如:文本折行后,不同的「行」会成为 render tree 种不同的 renderer),也有的 DOM 元素被 Render Tree 完全无视(例如: display:none 的元素)。
在建立 Render Tree 时,浏览器就要为每个 DOM Tree 中的元素根据 CSS 的解析结果(Style Rules)来确定生成怎样的 renderer。对于每个 DOM 元素,必须在所有 Style Rules 中找到符合的 selector 并将对应的规则进行合并。选择器的「解析」实际是在这里执行的,在遍历 DOM Tree 时,从 Style Rules 中去寻找对应的 selector。
解析规则
我们写的css样式可能会有很多,但是只需要在其中找到与dom元素相匹配的样式规则。正常情况下,只有少数的样式规则会匹配到目标dom元素,如何在众多的css样式中找到dom元素的样式规则显得尤其重要。
<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>
div > div.jartto p span.yellow{
color:yellow;
}
从右到左来解析选择器,一开始就过滤出跟目标节点最符合的集合出来,再在这个集合进行搜索,就可以大大降低了搜索空间。
- 首先就查找到 span.yellow 的元素;
- 接着向上查找符合 p 规则的元素,这样就又减少了集合的元素,只有符合当前的子规则才会匹配再上一条子规则;
- 由 p 再向上查找 div.jartto;
- 由 div.jartto 再向上查找 div,最后结束这个分支的遍历。
优点:减少无效匹配次数,从而匹配更快、性能更优。
css语法解析过程
大致过程如下:
- 先创建 CSSStyleSheet 对象。将 CSSStyleSheet 对象的指针存储到 CSSParser 对象中;
- CSSParser 识别出一个 simple-selector ,形如 “div” 或者 “.class”。创建一个 CSSParserSelector 对象;
- CSSParser 识别出一个关系符和另一个 simple-selecotr ,那么修改之前创建的 simple-selecotr, 创建组合关系符;
- 循环第3步直至碰到逗号或者左大括号;
- 如果碰到逗号,那么取出 CSSParser 的 reuse vector,然后将堆栈尾部的 CSSParserSelector 对象弹出存入 Vecotr 中,最后跳转至第2步。如果碰到左大括号,那么跳转至第6步;
- 识别属性名称,将属性名称的 hash 值压入解释器堆栈;
- 识别属性值,创建 CSSParserValue 对象,并将 CSSParserValue 对象存入解释器堆栈;
- 将属性名称和属性值弹出栈,创建 CSSProperty 对象。并将 CSSProperty 对象存入 CSSParser 成员变量m_parsedProperties 中;
- 如果识别处属性名称,那么转至第6步。如果识别右大括号,那么转至第10步;
- 将 reuse vector 从堆栈中弹出,并创建 CSSStyleRule 对象,CSSStyleRule 对象的选择符就是 reuse vector, 样式值就是 CSSParser 的成员变量 m_parsedProperties ;
- 把 CSSStyleRule 添加到 CSSStyleSheet 中;
- 清空 CSSParser 内部缓存结果;
- 如果没有内容了,那么结束。否则跳转值第2步。
解析内联样式
当 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;
}
重新组织后的数组数据为:
| selector | property | weight |
|---|---|---|
| a | color:yellow | 1 |
| p > a | color:red | 2 |
| p > a | background-color:black | 2 |
| div | margin:1px | 1 |
内敛样式只是 CSS 三种加载方式之一,不同的 CSS 加载方式产生的 Style rule ,通过权重来确定谁覆盖谁;內联样式与其他的加载样式方式唯一的区别就是权重不同。
computedStyle
在特定情况下,浏览器会共享 computedStyle,网页中能共享的标签非常多,所以能极大的提升执行效率!如果两个或多个 element 的 computedStyle 不通过计算可以确认他们相等,那么这些 computedStyle 相等的 elements 只会计算一次样式,其余的仅仅共享该 computedStyle 。
共享 computedStyle的条件:
- 该共享的element不能有id属性且CSS中还有该id的StyleRule.哪怕该StyleRule与Element不匹配。
- tagName和class属性必须一样;
- mappedAttribute必须相等;
- 不能使用sibling selector,譬如:first-child, :last-selector, + selector;
- 不能有style属性。哪怕style属性相等,他们也不共享;
总结
-
使用 id selector 非常的高效,在试用id selector时不需再加其他选择条件。
正确: #id1{ color: red; } 错误: div#id1{ color: red; } -
避免深层次的选择器。
正确: .msg{ color: red; } 错误: body > div > div > p.msg{ color: red; } -
慎用 ChildSelector 、属性选择器。
-
理解依赖继承,如果某些属性可以继承,那么自然没有必要在写一遍。