每天都用CSS,你真的懂CSS吗?

做为前端,咱们天天都在与CSS打交道,那么CSS的原理是什么呢?

1、浏览器渲染

开篇,咱们仍是不厌其烦的回顾一下浏览器的渲染过程,先上图:
css

正如上图所展现的,咱们浏览器渲染过程分为了两条主线:html

其一,HTML Parser 生成的 DOM 树;
其二,CSS Parser 生成的 Style Rules ;前端

在这以后,DOM 树与 Style Rules 会生成一个新的对象,也就是咱们常说的 Render Tree 渲染树,结合 Layout 绘制在屏幕上,从而展示出来。node

本文的重点也就集中在第二条分支上,咱们来探究一下 CSS 解析原理。

2、Webkit CSS 解析器

浏览器 CSS 模块负责 CSS 脚本解析,并为每一个 Element 计算出样式。CSS 模块虽小,可是计算量大,设计很差每每成为浏览器性能的瓶颈。
web

CSS 模块在实现上有几个特色:CSS 对象众多(颗粒小而多),计算频繁(为每一个 Element 计算样式)。这些特性决定了 webkit 在实现 CSS 引擎上采起的设计,算法。如何高效的计算样式是浏览器内核的重点也是难点。算法

先来看一张图:数组

Webkit 使用 Flex 和 Bison 解析生成器从 CSS 语法文件中自动生成解析器。浏览器

它们都是将每一个 CSS 文件解析为样式表对象,每一个对象包含 CSS 规则,CSS 规则对象包含选择器和声明对象,以及其余一些符合 CSS 语法的对象,下图可能会比较明了:缓存

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

可是咱们解析所要的结果是什么?

一、经过调用 CSSStyleSheet 的 parseString 函数,将上述 CSS 解析过程启动,解析完一遍后,把 Rule 都存储在对应的 CSSStyleSheet 对象中;

二、因为目前规则依然是不易于处理的,还须要将之转换成 CSSRuleSet。也就是将全部的纯样式规则存储在对应的集合当中,这种集合的抽象就是 CSSRuleSet;

三、CSSRuleSet 提供了一个 addRulesFromSheet 方法,能将 CSSStyleSheet 中的 rule 转换为 CSSRuleSet 中的 rule ;

四、基于这些个 CSSRuleSet 来决定每一个页面中的元素的样式;

3、CSS 选择器解析顺序

可能不少同窗都知道排版引擎解析 CSS 选择器时是从右往左解析,这是为何呢?

一、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 的元素。

二、在创建 Render Tree 时(WebKit 中的「Attachment」过程),浏览器就要为每一个 DOM Tree 中的元素根据 CSS 的解析结果(Style Rules)来肯定生成怎样的 renderer。对于每一个 DOM 元素,必须在全部 Style Rules 中找到符合的 selector 并将对应的规则进行合并。选择器的「解析」实际是在这里执行的,在遍历 DOM Tree 时,从 Style Rules 中去寻找对应的 selector。

三、由于全部样式规则可能数量很大,并且绝大多数不会匹配到当前的 DOM 元素(由于数量很大因此通常会创建规则索引树),因此有一个快速的方法来判断「这个 selector 不匹配当前元素」就是极其重要的。

四、若是正向解析,例如「div div p em」,咱们首先就要检查当前元素到 html 的整条路径,找到最上层的 div,再往下找,若是遇到不匹配就必须回到最上层那个 div,往下再去匹配选择器中的第一个 div,回溯若干次才能肯定匹配与否,效率很低。

对于上述描述,咱们先有个大概的认知。接下来咱们来看这样一个例子,参考地址:

<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;
}

对于上述例子,若是按从左到右的方式进行查找:

一、先找到全部 div 节点;
二、在 div 节点内找到全部的子 div ,而且是 class = “jartto”;
三、而后再依次匹配 p span.yellow 等状况;
四、遇到不匹配的状况,就必须回溯到一开始搜索的 div 或者 p 节点,而后去搜索下个节点,重复这样的过程。

这样的搜索过程对于一个只是匹配不多节点的选择器来讲,效率是极低的,由于咱们花费了大量的时间在回溯匹配不符合规则的节点。

若是换个思路,咱们一开始过滤出跟目标节点最符合的集合出来,再在这个集合进行搜索,大大下降了搜索空间。来看看从右到左来解析选择器:

一、首先就查找到 的元素;
二、紧接着咱们判断这些节点中的前兄弟节点是否符合 P 这个规则,这样就又减小了集合的元素,只有符合当前的子规则才会匹配再上一条子规则。

结果显而易见了,众所周知,在 DOM 树中一个元素可能有若干子元素,若是每个都去判断一下显然性能太差。而一个子元素只有一个父元素,因此找起来很是方便。

试想一下,若是采用从左至右的方式读取 CSS 规则,那么大多数规则读到最后(最右)才会发现是不匹配的,这样会作费时耗能,最后有不少都是无用的;而若是采起从右向左的方式,那么只要发现最右边选择器不匹配,就能够直接舍弃了,避免了许多无效匹配。

浏览器 CSS 匹配核心算法的规则是以 从右向左方式匹配节点的。这样作是为了减小无效匹配次数,从而匹配快、性能更优。

4、CSS 语法解析过程

CSS 样式表解析过程当中讲解的很细致,这里咱们只看 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 内部缓存结果。
1三、若是没有内容了,那么结束。不然跳转值第2步。

5、内联样式如何解析?

经过上文的了解,咱们知道,当 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我只是表示了他们之间的相对大小,并不是实际值。)

selector selector weight
a color:yellow 1
p > a color:red 2
p > a background-color:black 2
div margin:1px 3

好了,到这里,咱们来解决上述问题:
首先,要明确,内敛样式只是 CSS 三种加载方式之一;
其次,浏览器解析分为两个分支,HTML Parser 和 CSS Parser,两个 Parser 各司其职,各尽其责;
最后,不一样的 CSS 加载方式产生的 Style rule ,经过权重来肯定谁覆盖谁;

到这里就不难理解了,对浏览器来讲,內联样式与其余的加载样式方式惟一的区别就是权重不一样。

深刻了解,请阅读Webkit CSS引擎分析https://blog.csdn.net/scusyq/article/details/7059063

6、何谓 computedStyle ?

到这里,你觉得完了?Too young too simple, sometimes naive!

浏览器还有一个很是棒的策略,在特定状况下,浏览器会共享 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属性相等,他们也不共享;

span>p style="color:red">paragraph1span>p>
span>p style="color:red">paragraph2span>p>

固然,知道了共享 computedStyle 的规则,那么反面咱们也就了解了:不会共享 computedStyle 的规则,这里就不展开讨论了。

深刻了解,请参考:Webkit CSS 引擎分析 - 高效执行的 CSS 脚本https://blog.csdn.net/scusyq/article/details/7059063

7、眼见为实


如上图,咱们能够看到不一样的 CSS 选择器的组合,解析速度也会受到不一样的影响,你还会轻视 CSS 解析原理吗?

感兴趣的同窗能够参考这里:speed/validity selectors test for frameworks

8、有何收获?

一、使用 id selector 很是的高效。在使用 id selector 的时候须要注意一点:由于 id 是惟一的,因此不须要既指定 id 又指定 tagName:

Bad
p#id1 {color:red;}  
Good  
#id1 {color:red;}
固然,你非要这么写也没有什么问题,但这会增长 CSS 编译与解析时间,实在是不值当。

二、避免深层次的 node ,譬如:

Bad  
div > div > div > p {color:red;} 
Good  
p-class{color:red;}

三、慎用 ChildSelector ;

四、不到万不得已,不要使用 attribute selector,如:p[att1=”val1”]。这样的匹配很是慢。更不要这样写:p[id=”id1”]。这样将 id selector 退化成 attribute selector。

Bad  

p[id="id1"]{color:red;}  

p[class="class1"]{color:red;}  

Good 

#id1{color:red;}  

.class1{color:red;}

五、理解依赖继承,若是某些属性能够继承,那么天然没有必要在写一遍;
六、规范真的很重要,不只仅是可读性,也许会影响你的页面性能。这里推荐一个 CSS 规范http://nec.netease.com/standard/css-sort.html,能够参考一下。

9、总结

“学会使用”永远都是最基本的标准,可是懂得原理,你才能举一反三,超越自我。

源自:https://juejin.im/post/5e09bb3e6fb9a0162a0b8b76

声明:文章著做权归做者全部,若有侵权,请联系小编删除。

感谢 · 转发欢迎你们留言

本文分享自微信公众号 - web前端学习圈(web-xxq)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。