当前位置: 首页 > Web前端 > CSS

CSS每天都在用,那么CSS的原理是什么?

时间:2023-03-30 23:18:59 CSS

作为前端,我们每天都和CSS打交道,那么CSS的原理是什么?1、在浏览器渲染之初,我们还是不厌其烦的回顾一下浏览器的渲染过程。一、上图:如上图所示,我们的浏览器渲染过程分为两条主线:一是HTMLParser生成的DOM树;二、CSSParser生成的StyleRules;之后,DOM树和StyleRules会生成一个新的对象,也就是我们常说的RenderTree渲染树,结合Layout绘制到屏幕上进行显示。这篇文章的重点也在第二个分支,我们来探究一下CSS解析的原理。2、WebkitCSSparser浏览器CSS模块,负责解析CSS脚本,计算每个Element的样式。CSS模块虽然小,但计算量大,设计不当往往成为浏览器性能的瓶颈。CSS模块的实现有几个特点:CSS对象多(小且颗粒多)、计算频繁(为每个Element计算样式)。这些特点决定了webkit在实现CSS引擎时采用的设计和算法。如何高效地计算样式是浏览器内核的重点和难点。先看一张图:Webkit使用Flex和Bison解析器生成器,自动从CSS语法文件生成解析器。它们都是将每个CSS文件解析成一个样式表对象,每个对象包含CSS规则,CSS规则对象包含选择器和声明对象,以及其他一些符合CSS语法的对象,下图可能更清楚:Webkit使用自动编码generation工具生成相应的代码,也就是说自动生成词法分析和语法分析代码,Webkit中实现的CallBack函数在CSSParser中。CSS的一些解析函数的入口也在这里,它们会调用lex、parse等生成代码。相比之下,生成代码中需要的CallBack也需要在这里实现。例如,现在我们看其中一个回调函数createStyleRule()的实现,当需要创建通用规则时会调用该函数,代码如下:CSSRule*CSSParser::createStyleRule(CSSSelector*selector){CSSStyleRule*规则=0;如果(选择器){rule=newCSSStyleRule(styleElement);m_parsedStyleObjects。追加(规则);规则->setSelector(sinkFloatingSelector(selector));rule->setDeclaration(newCSSMutableStyleDeclaration(rule,parsedProperties,numPars));}清除属性();退货规则;从这个函数的实现可以清楚的看出,当解析器满足某种条件,需要创建一个CSSStyleRule时,就会调用这个函数。该函数的作用是创建一个CSSStyleRule,并设置它添加解析后的样式对象列表m_parsedStyleObjects,其中对象指的是Rule。这样,经过这样的解析,输入的样式表中的所有StyleRules都会被转换成Webkit内部的模型对象CSSStyleRule对象,保存在m_parsedStyleObjects中,它是一个Vector。但是我们要分析的结果是什么呢?1、通过调用CSSStyleSheet的parseString函数开始上述CSS解析过程。解析一次后,将Rules存放到对应的CSSStyleSheet对象中;2、由于现在的规则还是不好处理,需要转成CSSRuleSet。即把所有的纯样式规则存放在对应的集合中,这个集合的抽象就是CSSRuleSet;3、CSSRuleSet提供了一个addRulesFromSheet方法,可以将CSSStyleSheet中的规则转换为CSSRuleSet中的规则;4.根据这些CSSRuleSet来确定每个页面中元素的样式;3.CSS选择器解析顺序很多同学可能都知道,排版引擎是从右到左解析CSS选择器的。为什么是这样?1.解析HTML生成DOMTree(我们比较熟悉);CSS解析完成后,需要将解析结果连同DOMTree的内容一起分析构建RenderTree,最终用于绘制。RenderTree中的元素(在WebKit中称为“renderers”,在Firefox中称为“frames”)与DOM元素一一对应,但不是一一对应的:一个DOM元素可能对应多个renderer,比如textwrapping,不同的“rows”"将成为渲染树中的不同渲染器。某些DOM元素会被RenderTree完全忽略,例如display:none元素。2、在创建RenderTree(WebKit中的“Attachment”过程)时,浏览器必须根据CSS分析结果(StyleRules)来决定为DOMTree中的每个元素生成什么样的renderer。对于每个DOM元素,必须在所有样式规则中找到匹配的选择器,并且必须合并相应的规则。选择器的“解析”其实就是在这里进行的。在遍历DOMTree时,从StyleRules中找到对应的选择器。3、因为所有样式规则的数量可能很多,而且大部分都不会匹配到当前DOM元素(因为数量多,一般会建一个规则索引树),所以有个快速判断的方法“此选择器与当前元素不匹配”非常重要。4、如果是正向分析,比如“divdivpem”,我们首先需要检查当前元素到html的整个路径,找到最上面的div,然后往下看,如果不匹配,就要返回bottom上层的div往下匹配selector中的第一个div,还需要往回走几次才能判断是否匹配,效率很低。对于上面的描述,我们先有个大概的了解。接下来我们看这样一个例子,参考地址:

span>111span>

span>222span>

333

444

CSS选择器:div>div.jarttopspan.yellow{color:yellow;}上面的例子,如果从左到右查找:1.先找到所有的div节点;2、找到div节点中的所有子div,class="jartto";3.然后依次匹配pspan.yellow等;4.遇到不匹配时,必须回到一开始查找的div或p节点,再查找下一个节点,重复这个过程。这样的搜索过程对于只匹配少数几个节点的选择器来说是极其低效的,因为我们花了很多时间回溯来匹配不符合规则的节点。如果我们换一种思路,先筛选出与目标节点最匹配的集合,然后在这个集合中搜索,这样就大大减少了搜索空间。下面从右到左看一下解析选择器:1.先找到元素;2.然后判断这些节点的前兄弟节点是否符合P的规则,再次减少集合的元素,只有满足当前子规则,才会匹配上一个子规则。结果是显而易见的。众所周知,一个元素在DOM树中可能有多个子元素。如果每一个都评判,表现显然太差了。而且一个子元素只有一个父元素,查找起来非常方便。试想一下,如果CSS规则是从左到右读取的,那么大部分规则要到最后(最右边)才能找到匹配,这会耗费时间和精力,而且很多到最后都没有用;但是如果采用从右到左的方式,只要发现最右边的选择器不匹配,就可以直接丢弃,避免了很多无效的匹配。浏览器CSS匹配核心算法的规则是从右到左匹配节点。这样做是为了减少无效匹配的数量,使匹配更快,性能更好。4.CSS语法分析过程CSS样式表分析过程非常详细。这里我们只看CSS语法解释器。大致流程如下:1.首先创建一个CSSStyleSheet对象。在CSSParser对象中存储指向CSSStyleSheet对象的指针。2.CSSParser识别“div”或“.class”形式的简单选择器。创建一个CSSParserSelector对象。3.CSSParser识别出一个关系和另一个simple-selecotr,然后修改之前创建的simple-selecotr创建一个组合关系。4.重复第3步,直到遇到逗号或左大括号。5.如果遇到逗号,则取出CSSParser的reusevector,将栈尾的CSSParserSelector对象pop到Vecotr中,最后跳转到第2步,如果遇到左大括号,则跳转到第6步。6.识别属性名称并将属性名称的哈希值推送到解释器堆栈。7.识别属性值,创建一个CSSParserValue对象,并将CSSParserValue对象存储在解释器栈中。8.从堆栈中弹出属性名称和属性值以创建一个CSSProperty对象。并将CSSProperty对象存放在CSSParser的成员变量m_parsedProperties中。9.如果属性名称被识别,转到第6步。如果右大括号被识别,转到第10步。10.从堆栈中弹出重用向量并创建一个CSSStyleRule对象。CSSStyleRule对象的选择器是复用向量,样式值是CSSParser的成员变量m_parsedProperties。11.将CSSStyleRule添加到CSSStyleSheet。12.清空CSSParser内部缓存结果。13.如果没有更多内容,则结束。否则跳到第2步。5.如何解析内联样式?通过上面的理解,我们知道CSSParser在解析CSS脚本时,会生成CSSStyleSheetList,保存在Document对象上。必须重新组织这些CSSStyleSheetList,以便更快地计算样式。计算样式就是从CSSStyleSheetList中找出所有匹配对应元素的属性值对。匹配会被CSSSelector校验,需要满足级联规则。将声明中的所有属性组织成一个大数组。数组中的每一项记录了属性的选择器、属性的值和权重(级联规则)。它可能类似于以下表现:p>a{color:red;background-color:black;}a{color:yellow}div{margin:1px;}重组后的数组数据为(我只是表示了它们之间的权重selectorweight的相对大小并不是实际值。)selectorselectorweightacolor:yellow1p>acolor:red2p>abackground-color:black2divmargin:1px3好了,下面我们来解决上面的问题:首先要明确的是,内向风格只是CSS1的三种加载方式之一,其次,浏览器解析是分为两个分支,HTMLParser和CSSParser。两个Parser各司其职;最后,不同CSS加载方式生成的Style规则由权重决定。WHO;这里不难理解。对于浏览器来说,内联样式和其他加载样式的唯一区别就是权重。深入理解请阅读WebkitCSS引擎解析6.什么是computedStyle?过来,你以为结束了吗?太年轻太单纯,有时天真!浏览器也有一个很好的策略。在某些情况下,浏览器会共享computedStyle。网页中有很多可以共享的标签,可以大大提高执行效率!如果可以共享,那么就不需要执行匹配算法了,执行效率自然很高。也就是说:如果两个或多个元素的computedStyle没有通过计算确认相等,那么这些computedStyle相等的元素只会计算一次样式,其余的只会共享computedStyle。那么哪些规则共享computedStyle呢?共享元素不能有id属性,即使StyleRule与元素不匹配,CSS中也有id的StyleRule。tagName和class属性必须相同;mappedAttribute必须相等;兄弟选择器不能使用,如:first-child,:last-selector,+selector;没有样式属性。即使样式属性相等,它们也不共享;消极的一面我们也理解:computedStyle的规则不会共享,这里就不讨论了。深入理解请参考:WebkitCSS引擎解析——高效执行CSS脚本之七。眼见为实如上图所示,我们可以看到不同CSS选择器的组合,解析速度也会受到不同的影响。你也会鄙视CSS你分析原理?有兴趣的同学可以参考这里:speed/validityselectorstestforframeworks8.有什么收获?1.使用id选择器非常高效。使用id选择器时需要注意一件事:因为id是唯一的,所以不需要同时指定id和tagName:Badp#id1{color:red;}Good#id1{color:red;}当然,你非要这样写没有什么错,只是会增加CSS的编译和解析时间,实在是不值得。2.避免深节点,例如:Baddiv>div>div>p{color:red;}Goodp-class{color:red;}3.慎用ChildSelector;4.除非绝对必要,否则不要使用属性选择器,例如:p[att1="val1"]。这样的匹配非常慢。甚至不要这样写:p[id="id1"]。这会将id选择器退化为属性选择器。坏p[id="id1"]{color:red;}p[class="class1"]{color:red;}好#id1{color:red;}.class1{color:red;}5.理解依赖关系继承,如果有些属性是可以继承的,那自然就不用再写了;6.规范真的很重要,不仅仅是可读性,它可能会影响你的页面性能。这里推荐一个CSS规范,大家可以参考一下。9、总结“学会使用”永远是最基本的标准,但只有明白其中的原理,才能触类旁通,超越??自我。