天天都用CSS,你真的懂CSS吗?
作为前台,我们每天都在与CSS打交道,那么CSS的原理是什么呢?
一、浏览器渲染
开篇,我们还是不厌其烦的回顾一下浏览器的渲染过程,先上图:

正如上图所展现的,我们浏览器渲染过程分为了两条主线:
其一,HTML Parser 生成的 DOM 树;
其二,CSS Parser 生成的 Style Rules ;
在这之后,DOM 树与 Style Rules 会生成一个新的对象,也就是我们常说的 Render Tree 渲染树,结合 Layout 绘制在屏幕上,从而展示出来。
本文的重点也就集中在第二条分支上,我们来探索一下 CSS 解析原理。
二、Webkit CSS 解析器
浏览器 CSS 模块负责 CSS 脚本解析,并为每个 Element 计算出样式。CSS 模块虽小,但是计算量大,设计不好往往成为浏览器性能的瓶颈。
CSS 模块在实现上有几个特点:CSS 对象众多(颗粒小而多),计算频繁(为每个 Element 计算样式)。这些特性决定了 webkit 在实现 CSS 引擎上采取的设计,算法。如何高效的计算样式是浏览器内核的重点也是难点。
先来看一张图:

Webkit 使用 Flex 和 Bison 解析生成器从 CSS 语法文件中自动生成解析器。
它们都是将每个 CSS 文件解析为样式表对象,每个对象包含 CSS 规则,CSS 规则对象包含选择器和公告对象,以及其余少量符合 CSS 语法的对象,下图可能会比较明了:

Webkit 使用了自动代码生成工具生成了相应的代码,也就是说词法分析和语法分析这部分代码是自动生成的,而 Webkit 中实现的 CallBack 函数就是在 CSSParser 中。
CSS 的少量解析功能的入口也在此处,它们会调用 lex , parse 等生成代码。相对的,生成代码中需要的 CallBack 也需要在这里实现。
举例来说,现在我们来看其中一个回调函数的实现,createStyleRule(),该函数将在一般性的规则需要被建立的时候调用,代码如下:

从该函数的实现可以很清楚的看到,解析器达到某条件需要创立一个 CSSStyleRule 的时候将调用该函数,该函数的功能是创立一个 CSSStyleRule ,并将其增加已解析的样式对象列表m_parsedStyleObjects中去,这里的对象就是指的 Rule 。
那么如此一来,经过这样一番解析后,作为输入的样式表中的所有 Style Rule 将被转化为 Webkit 的内部模型对象 CSSStyleRule 对象,存储在m_parsedStyleObjects中,它是一个Vector。
但是我们解析所要的结果是什么?
1、通过调用 CSSStyleSheet 的 parseString 函数,将上述 CSS 解析过程启动,解析完一遍后,把 Rule 都存储在对应的 CSSStyleSheet 对象中;
2、因为目前规则仍然是不易于解决的,还需要将之转换成 CSSRuleSet。也就是将所有的纯样式规则存储在对应的集合当中,这种集合的笼统就是 CSSRuleSet;
3、CSSRuleSet 提供了一个 addRulesFromSheet 方法,能将 CSSStyleSheet 中的 rule 转换为 CSSRuleSet 中的 rule ;
4、基于这些个 CSSRuleSet 来决定每个页面中的元素的样式;
三、CSS 选择器解析顺序
可能很多同学都知道排版引擎解析 CSS 选择器时是从右往左解析,这是为什么呢?
1、HTML 经过解析生成 DOM Tree(这个我们比较熟习);而在 CSS 解析完毕后,需要将解析的结果与 DOM Tree 的内容一起进行分析建立一棵 Render Tree,最终用来进行绘图。Render Tree 中的元素(WebKit 中称为「renderers」,Firefox 下为「frames」)与 DOM 元素相对应,但非逐个对应:一个 DOM 元素可能会对应多个 renderer,如文本折行后,不同的「行」会成为 render tree 种不同的 renderer。也有的 DOM 元素被 Render Tree 完全无视,比方 display:none 的元素。
2、在建立 Render Tree 时(WebKit 中的「Attachment」过程),浏览器就要为每个 DOM Tree 中的元素根据 CSS 的解析结果(Style Rules)来确定生成怎么的 renderer。对于每个 DOM 元素,必需在所有 Style Rules 中找到符合的 selector 并将对应的规则进行合并。选择器的「解析」实际是在这里执行的,在遍历 DOM Tree 时,从 Style Rules 中去寻觅对应的 selector。
3、由于所有样式规则可能数量很大,而且绝大多数不会匹配到当前的 DOM 元素(由于数量很大所以一般会建立规则索引树),所以有一个快速的方法来判断「这个 selector 不匹配当前元素」就是极其重要的。
4、假如正向解析,例如「div div p em」,我们首先就要检查当前元素到 html 的整条路径,找到最上层的 div,再往下找,假如遇到不匹配就必需回到最上层那个 div,往下再去匹配选择器中的第一个 div,回溯若干次才能确定匹配与否,效率很低。
对于上述形容,我们先有个大概的认知。接下来我们来看这样一个例子,参考地址:

CSS 选择器:

对于上述例子,假如按从左到右的方式进行查找:
1、先找到所有 div 节点;
2、在 div 节点内找到所有的子 div ,并且是 class = “jartto”;
3、而后再依次匹配 p span.yellow 等情况;
4、遇到不匹配的情况,就必需回溯到一开始搜索的 div 或者者 p 节点,而后去搜索下个节点,重复这样的过程。
这样的搜索过程对于一个只是匹配很少节点的选择器来说,效率是极低的,由于我们花费了大量的时间在回溯匹配不符合规则的节点。
假如换个思路,我们一开始过滤出跟目标节点最符合的集合出来,再在这个集合进行搜索,大大降低了搜索空间。来看看从右到左来解析选择器:
1、首先就查找到 的元素;
2、紧接着我们判断这些节点中的前兄弟节点能否符合 P 这个规则,这样就又减少了集合的元素,只有符合当前的子规则才会匹配再上一条子规则。
结果显而易见了,众所周知,在 DOM 树中一个元素可能有若干子元素,假如每一个都去判断一下显然性能太差。而一个子元素只有一个父元素,所以找起来非常方便。
试想一下,假如采用从左至右的方式读取 CSS 规则,那么大多数规则读到最后(最右)才会发现是不匹配的,这样会做费时耗能,最后有很多都是无用的;而假如采取从右向左的方式,那么只需发现最右边选择器不匹配,即可以直接舍弃了,避免了许多无效匹配。
浏览器 CSS 匹配核心算法的规则是以从右向左方式匹配节点的。这样做是为了减少无效匹配次数,从而匹配快、性能更优。
四、CSS 语法解析过程
CSS 样式表解析过程中讲解的很细致,这里我们只看 CSS 语法解释器,大致过程如下:
1、先创立 CSSStyleSheet 对象。将 CSSStyleSheet 对象的指针存储到 CSSParser 对象中。
2、CSSParser 识别出一个 simple-selector ,形如 “div” 或者者 “.class”。创立一个 CSSParserSelector 对象。
3、CSSParser 识别出一个关系符和另一个 simple-selecotr ,那么修改之前创立的 simple-selecotr, 创立组合关系符。
4、循环第3步直至碰到逗号或者者左大括号。
5、假如碰到逗号,那么取出 CSSParser 的 reuse vector,而后将堆栈尾部的 CSSParserSelector 对象弹出存入 Vecotr 中,最后跳转至第2步。假如碰到左大括号,那么跳转至第6步。
6、识别属性名称,将属性名称的 hash 值压入解释器堆栈。
7、识别属性值,创立 CSSParserValue 对象,并将 CSSParserValue 对象存入解释器堆栈。
8、将属性名称和属性值弹出栈,创立 CSSProperty 对象。并将 CSSProperty 对象存入 CSSParser 成员变量m_parsedProperties 中。
9、假如识别处属性名称,那么转至第6步。假如识别右大括号,那么转至第10步。
10、将 reuse vector 从堆栈中弹出,并创立 CSSStyleRule 对象。CSSStyleRule 对象的选择符就是 reuse vector, 样式值就是 CSSParser 的成员变量 m_parsedProperties 。
11、把 CSSStyleRule 增加到 CSSStyleSheet 中。
12、清空 CSSParser 内部缓存结果。
13、假如没有内容了,那么结束。否则跳转值第2步。
五、内联样式如何解析?
通过上文的理解,我们知道,当 CSS Parser 解析完 CSS 脚本后,会生成 CSSStyleSheetList ,他保存在Document 对象上。为了更快的计算样式,必需对这些 CSSStyleSheetList 进行重新组织。
计算样式就是从 CSSStyleSheetList 中找出所有匹配相应元素的 property-value 对。匹配会通过CSSSelector 来验证,同时需要满足层叠规则。
将所有的 declaration 中的 property 组织成一个大的数组。数组中的每一项纪录了这个 property 的selector,property 的值,权重(层叠规则)。
可能相似如下的体现:

重新组织之后的数组数据为(weight我只是表示了他们之间的相对大小,并非实际值。)

好了,到这里,我们来处理上述问题:
首先,要明确,内敛样式只是 CSS 三种加载方式之一;
其次,浏览器解析分为两个分支,HTML Parser 和 CSS Parser,两个 Parser 各司其职,各尽其责;
最后,不同的 CSS 加载方式产生的 Style rule ,通过权重来确定谁覆盖谁;
到这里就不难了解了,对浏览器来说,內联样式与其余的加载样式方式唯一的区别就是权重不同。
六、何谓 computedStyle ?
到这里,你以为完了?Too young too simple, sometimes naive!
浏览器还有一个非常棒的策略,在特定情况下,浏览器会共享 computedStyle,网页中能共享的标签非常多,所以能极大的提升执行效率!假如能共享,那就不需要执行匹配算法了,执行效率自然非常高。
也就是说:假如两个或者多个 element 的 computedStyle 不通过计算可以确认他们相等,那么这些 computedStyle 相等的 elements 只会计算一次样式,其他的仅仅共享该 computedStyle 。
那么有哪些规则会共享 computedStyle 呢?
1、该共享的element不能有id属性且CSS中还有该id的StyleRule.哪怕该StyleRule与Element不匹配。
2、tagName和class属性必需一样;
3、mappedAttribute必需相等;
4、不能使用sibling selector,譬如:first-child, :last-selector, + selector;
5、不能有style属性。哪怕style属性相等,他们也不共享;

当然,知道了共享 computedStyle 的规则,那么反面我们也就理解了:不会共享 computedStyle 的规则,这里就不开展探讨了。
深入理解,请参考:Webkit CSS 引擎分析 – 高效执行的 CSS 脚本https://blog.csdn.net/scusyq/article/details/7059063
七、眼见为实

如上图,我们可以看到不同的 CSS 选择器的组合,解析速度也会受到不同的影响,你还会轻视 CSS 解析原理吗?
八、有何收获?
1、使用 id selector 非常的高效。在使用 id selector 的时候需要注意一点:由于 id 是唯一的,所以不需要既指定 id 又指定 tagName:

当然,你非要这么写也没有什么问题,但这会添加 CSS 编译与解析时间,实在是不值当。
2、避免深层次的 node ,譬如:

3、慎用 ChildSelector ;
4、不到万不得已,不要使用 attribute selector,如:p[att1=”val1”]。这样的匹配非常慢。更不要这样写:p[id=”id1”]。这样将 id selector 退化成 attribute selector。

5、了解依赖继承,假如某些属性可以继承,那么自然没有必要在写一遍;
6、规范真的很重要,不仅仅是可读性,也许会影响你的页面性能。这里推荐一个 CSS?
九、总结
“学会使用”永远都是最基本的标准,但是懂得原理,你才能触类旁通,超越自我。
我目前是在职前台开发,假如你现在也想学习前台开发技术,在入门学习前台的过程当中有遇见任何关于学习方法,学习路线,学习效率等方面的问题,你都可以申请加入我的前台学习交流裙:前面:603 中间:985 最后:993。里面聚集了少量正在自学前台的初学者,裙文件里面也有我做前台技术这段时间整理的少量前台学习手册,前台面试题,前台开发工具,PDF文档书籍教程,需要的话都可以自行来获取下载。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 天天都用CSS,你真的懂CSS吗?