深入CSS选择器
CSS选择器用于选取目标DOM节点以便对该其增加样式,更改其在网页的展示效果。如今的网页CSS只有一个作用域,即根作用域,具有全局特性。
# 选择符
-
" "
(空格):后代选择符,选取所有子元素 -
>
(尖括号):父子选择符,选取第一代子元素 -
+
(加号):相邻兄弟选择符,选取紧邻的指定的下一个兄弟元素 -
~
(波浪号):随后兄弟选择符,选取随后的指定的兄弟元素,无需紧邻关系 -
||
(双管道号):列关系选择符,选取某选择器下的某个列元素,支持跨列元素。(普通选择器不支持跨列)
> 有你不知
(1)还有没空格的如 .cs-box.cs-header
,什么意思?这是选择器的级联语法,使用在当需要提高当前选择器的优先级,又不想影响其他元素的场景。一般只建议用在重复自身上如.cs-box.cs-box
。另外,标签选择器不支持级联语法。
(2)+
和 ~
都是兄弟选择符,选择内容范围有所不同,但有一点相同的是他们都不能选择之前的兄弟元素。为什么?早前CSS也提出过选择前面兄弟的办法,也就是:has()
,不过各浏览器不肯支持,近乎被抛弃了。原因是受限于DOM的渲染规则。其解析顺序自上而下,由外到里,对于已构建且渲染好的元素进行重新渲染,不仅影响自身还会影响其下所有子元素,严重拖累页面性能,导致浏览器长时间白板,造成不好的体验。
> 脱坑实战
阅读以下样式代码,判断<p>
标签是否能渲染上颜色?
<h4>标题</div>
这里是文本节点,检查相邻选择符效果
<p>被检查的内容</p>
h4 + p {
color: skyblue;
}
在JSFiddle试试,答案是肯定的。也就是说,相邻兄弟选择器在判断“紧邻关系”时会忽略文本节点,p
标签中的文案变成了天蓝色。
# 选择器权重
常说的选择器分三种,这里想说的更细一点,以下使用选择器优先级排序:
-
通配符、组合、选择符:权重
0
*
,+
,~
,>
," "
,:not()
,:is()
不懂?,:where
不懂?往下看 等 -
标签选择器:权重
1
body
,div
,p
,span
,a
等 -
类选择器,属性选择器,伪类:权重
10
.foo
,a[href]
,:hover
,img[alt="default"]
等 -
ID选择器:权重
100
#foo
,#header
等 -
内联样式:权重:优先于任何外部样式,除
!important
<span class="cs-span" style="color: red">
-
!important:权重:最高优先级
不建议使用在css样式中,仅推荐在JS中需设置样式覆盖的场景
> 有你不知
(1)类选择器的权重为 10
, ID选择器的权重为 100
,那是不是说我嵌套10个类就可以与ID选择器平起平坐了?如下设置:
#cs-id-me {
color: blue;
}
.cs-me.cs-me.cs-me.cs-me.cs-me.cs-me.cs-me.cs-me.cs-me.cs-me {
color: wheat;
}
你可以在CodePen亲自试下,答案是否定的。
我们可以认为,不同权重的选择器之间的差距是无法跨越的存在。其实以前 Chrome 和 FF 也出现过权重越级的问题。早期,它们所有的类名都是以8个字节存储的,8字节最大容量就是255,当出现连续256个类名的时候,必然会溢出到ID区域,从而产生识别异常的现象。而现在提高到16个字节,就不会再出现这个问题了。
所以,对于权重计算的问题,应遵循如下原则:
(1)首先要看落地元素的选择器类型,如果不同,直接判断不可跨越,输出样式
(2)若相同,再看使用选择符连接的其他选择器权重之和,输出样式
(3)如果权重之和计算相同,根据 后来居上 原则执行样式覆盖,输出样式
PS:另外在实际开发中相信也没有那个小伙伴会书写多达十多个的选择符。反正笔者脑子没进水~
(2)选择器使用对页面渲染性能的负担一致吗?答案是否定的。从好到差排序如下:
ID选择器 > 类选择器 > 标签选择器 > 通配符 > 属性选择器 > 部分伪类
> 脱坑实战
有样式设置如下,请问<p>
显示什么颜色?
<div class="outter">
<div class="inner">
<p>文字显示什么颜色</p>
</div>
</div>
.inner p { coilor: darkblue; }
.outter p { color: lightblue; }
很多人会认为是“深蓝色”。认为 .inner
离 p
比较近,优先响应,其实不然。在选择器权重的规则里并没有就近原则这么一项。正确的做法是分析选择器权重是多少
本例中,使用了后代选择器来描述,首先还是要看最终落地元素是谁?是p
,这是一个标签选择器,两个类都是p
,没有跨级。再者,计算.inner p
,权重为1 + 10 = 11
, .outter p
也是 11
,权重相同,再根据后来居上,因此,得到<p>
标签最终样式为“浅蓝色”。
# 属性匹配选择器
属性选择器对页面性能并不友好,但功能却很强大。它的几种用法,注意中括号内部符号是没有留空格的。如下:
-
[attr]:属性选择器。属性存在即选中,常用于如
disbaled
这种布尔属性
[disabled] {}
,[checked] {}
-
[attr="val"]:属性值全匹配选择器,需注意匹配类似
class="cs-1 cs-2"
时,val
应为全值,而不是某个单词。
[class="cs-1 cs-2"] {}
-
[attr~="val"]:属性值单词全匹配选择器,还是上诉案例
[class~="cs-1"] {}
,[class~="cs-2"] {}
-
[attr|="val"]:属性值起始片段全匹配选择器。所谓的起始片段,要么全匹配
val
,要么匹配第一个短杆及之前部分内容。如类似
<div class="cs"></div>
<div class="cs-1"></div>
匹配第一项:[class|="cs"]{}
,这个选择器并不会选中第二项
匹配第二项: [class|="cs-"]{}
-
属性值字符正则匹配选择器:该选择器的匹配目标是字符级别,是最灵活的一种匹配方式。包含三种
-
前匹配
[attr^="val"]
:常用作判断<a>
元素的链接地址类型
链接类型:[href^="http"]{}
,[href^="ftp"]{}
,[href^="//"] {}
网页锚点:[href^="#"]{ background: url(./icon-anchor.svg) no-repeat left; }
手机或邮箱:[href^="tels:"]{}
,[href^="mailto:"]{}
-
后匹配
[attr$="val"]
:常用作判断<a>
元素的链接文件类型
PDF/ZIP文件:[href$=".pdf"]{}
,[href^=".zip"]{}
图片:[href$=".png"], [href^=".jpg"], [href^=".gif"] {}
-
任意匹配
[attr\*="val"]
,最灵活、最常用、最重要
判断某个元素是否隐藏:[style*="display: none"]{}
配合自定义属性实现查找:[data-search*="uesr_input"] {}
。该实践后续有实例
-
前匹配
> 忽略大小写
因为属性值的匹配是区分大小写的,如果先要忽略大小写,可以使用字符i
或I
来实现,建议用i
,如:
[attr~="val" i] {}
,[attr*="val" i] {}
# CSS命名及渲染规则
CSS本身并没有什么语法,因此也没有什么命名规范,这就像一个哲学问题,佛说佛有理。但这不代表所有CSS的代码质量都是一样的。仍有很多原则可循。
通常一个网站的页面样式,可以归分为这几模块:公用结构、公用模块、UI组件、精致布局和细节处理。1、2、3、5模块通常会各用一个样式文件描述,精致布局跟随页面。
而对于CSS选择器的命名,也有两种思路:
(1)基于语义化:突出DOM节点功能。 头部 cs-header
,内容 cs-content
,盒子 cs-box
(2)基于CSS属性:突出样式功能。 距顶10px cs-mt-10
,居中显示 cs-tc
text-align:center;,flex布局 cs-df
display:flex;
对于公用结构、公用模块、UI组件、精致布局
适合使用语义化命名,对于细节处理
,建议使用基于CSS属性来实现。为什么?两种思路中,语义化一般包含整个结构模块样式,含多个样式描述的组合;而基于CSS属性通常只有一条样式描述。对于如精致布局如果使用后者,就会产生一堆的类名组合类似
<div class="cs-mt-10 cs-tc cs-df"></div>
这不是最佳体验,如果真的这么写,还不如在页面上直接内联样式。同理,对于细节修饰。一般只有某一个样式,如,已使用逗号分隔的选择器样式,默认文案都是居中显示,现在要求其中一个选择器的文案居左显示,如果是基于语义化,如下
.cs-header-right-box {
text-align: left;
}
对于这个样式,没有错,但,这个类选择器还会有复用的价值吗?显然,几乎没有。既然如此,定义一个这么长的名称,仅写了一个样式描述,意义何在?相反,仅对于这些高频使用的样式描述,定义成独立的简单易记的类名,需要使用时引入,不就回归了类名初衷-可复用-的目的了么。如本例,在公用模块中添加如下:
.cs-tl {
text-align: left;
}
# 统一识别前缀
另外,为什么笔者均加了前缀cs-
? 正是因为现在的网页CSS只有一个作用域,对于不同的开发人员,或引用的不同的UI或样式库,为了避免发生样式冲突和减少对别人的影响,利用cs-
的前缀形成一个类似命名空间,达到样式隔离的效果。
# 大小写问题
CSS选择器是否区分大小写?答案是:有的区分,有的不区分。笔者不是再讲废话,是否有大小写是遵循html规则而来的。
对于HTML来说,标签和属性名都是不区分大小写的,而属性值区分;CSS同规则,标签选择器和属性选择器不区分,而类选择器,ID选择器因为取的是属性的值,区分大小写。
也有手段可以设置大小写不敏感,如[class~="val" i]
$ 组合选择器(组合伪类)
组合选择符主要用于对已有选择器进行修饰,变更,过滤等手段来实现单个选择器无法实现的选择功能,主要包含:not()
,:is()
,:any()
,:where()
,:has()
等
1. 否定伪类 :not()
接受一个选择器参数,如果当前元素与括号里面的选择器不匹配,则该选择器会对其匹配选中。该伪类优先级为0
,其优先级的计算取决于括号里的表达式。如下,取决于p
标签,权重为1
。
:not(p) {}
:not()
支持级联用法,各选择条件呈与关系,如下表示:匹配所有不处于禁用,且不处于只读状态的<input>
元素。
input:not(:disabled):not(:read-only) {}
需要注意的是,:not()
不支持多个选择器,如下是错误的
input:not(:disabled, :read-only) {} // 错误
> 有你不知
(1)否定伪类优先级为0
,有啥用?只要想法正确,其实非常有用。通常其他方案实现的相同样式需要多个选择器通过选择符连接,如后代选择符,相邻兄弟选择符或伪类(如:first-child
)等,虽效果相同,但提高了本元素样式的优先级,很可能因此覆盖了其他元素效果。
(2)否定伪类的最大作用就是优化以前我们重置 CSS 样式的策略。由于重置样式在开发中非常常见,因此:not()
很常用,也较常见到它。举个例子:
假设有场景:一个页面有几个面板由Tab的选项卡控制显示隐藏,用户通过点击某个Tab选项,来展示对应的面板信息,往常实现如下。
.cs-panel { display: none; }
.cs-panel.active { display: block; }
使用否定伪类可以实现如下:
.cs-panel:not(.active) { display: none; }
同时注意兼容性问题,IE8一下不支持该伪类,需hack
还有个典型的案例。假设有一个列表,我们希望除了第一个,其他项都距顶10px
,实现如下
.cs-li { margin-top: 10px; }
.cs-li:first-child { margin-top: 0; }
或.cs-li:nth-child(1)
或.cs-li:nth-of-type(1)
,具体看真实业务场景。使用否定伪类如下:
.cs-li:not(:first-child) { margin-top: 10px; }
对于该例,其实还可以使用相邻兄弟选择符实现,即 .cs-li + .cs-li
,但该方案仍有调高权重的劣势。
2. 任意匹配伪类 :is()
任意匹配伪类可以把括号中的选择器依次分配出去,匹配括号外的选择器构成最终选择器列表,适用于有很多逗号分隔的选择器场景,可以理解是一种简化效果。
:is()
选择器是由:matches()
更名而来的,而这个:matches()
,又是由:any()
更名而来的~~。它们三者并不是简单的更名关系,其演化史如下::any()
最早使用,说被废弃了也不准确,现在除IE/Edge不支持,其他都还支持(需要加私有前缀如Chrome的-webkit-
)。但它的优先级有些混乱,:any()
会忽略括号里选择器的优先级,把连同:any()
在内一同计算为一个普通伪类优先级,也就是权重10
,直接忽视括号内部选择器是其造成混乱的罪魁祸首,也是被抛弃的主要原因。:metches()
更名主要是因为名称语义不太准确,不如:is()
来的简短明了,而:is()
另一个优势是与:not()
形成了呼应。从而晋级主流用法。注意,主流浏览器已支持:is()
但还未全面普及,:metches()
已不被支持识别。
:is()
伪类本身优先级同:not()
为 0
,整个表达式由参数优先级中最高的那个决定的。如下:
:is(.article, section) p {}
以上优先级等同于.article p {}
,权重为 10 + 1 = 11
。其主要作用就是简化选择器,如下
.cs-art-a > img,
.cs-art-b > img,
.cs-art-c > img {}
/* 使用 :is()方案 */
:is(.cs-art-a, .cs-art-b, .cs-art-c) >img {}
一维的简化写法并不能体现:is()
的强大之处,它还能用于多维写法,分别判断第一、第二个位置的落地值,如下:
ol ol li,
ol ul li,
ul ul li,
ul ol li {}
/* 使用:is()改写如下 */
:is(ol, ul) :is(ol, ul) li {}
3. 任意匹配伪类 :where 了解
怎么和:is()
都叫任意匹配伪类?是不是写错了?并没有。:where
与:is()
在写法,用法,含义,作用等都是一样的。不同的是:where
伪类的优先级永远是0。例如
:where(.article, section) p {}
计算上例优先级时,等同于p
的优先级,也就是1
。再如下,等同于 .box
选择器,也是就是10
。
:where(#article, #section) .box {}
对比:where
与:any()
区别,:any()
永远计算权重为10
,:where()
永远计算权重为0
,似乎没什么变化,但为什么:any()
被弃用了而:where()
却活的很好?
原因正是这个权重不同造成。被:any()
加上的权重值不仅增加了整个选择器的优先级,又不能群峰不同类型的选择器组合的差异,可以说几乎一无是处。而:where()
权重为0
,不仅能选择到心仪的目标DOM节点,又能避免优先级身高对其他元素造成威胁,具有可取之处。
4. 关联伪类 :has 了解
正如前文所说,:has()
规范其实定制的很早,但浏览器一直不肯支持。原因是因为它可以实现类似“父选择器”和“前面兄弟选择器”的功能,这极大的威胁了DOM渲染解析的性能效率问题。如下:
(1)a:has(< svg)
匹配包含<svg>
元素的<a>
元素,这是选择父元素。
(2)h1:has(+ p)
匹配后面跟随<p>
元素的<h1>
元素,这是选择前面元素。
# 选择符实现低成本交互效果
使用选择符最硬核的应用还是配合诸多伪类实现很多实用的交互效果,是众多高级选择器技术的核心。
> 实战案例
(1) 当输入框聚焦时,希望现实后面的提示文字。在JQ中可能通过js实现,在Vue中可能通过数据绑定实现,而这里,笔者想通过css实现:
用户名: <input> <span class="cs-input-tips">不超过10个文字</span>
.cs-input-tips {
color: gray;
margin-left: 15px;
position: absolute;
visibility: hidden;
}
:focus + .cs-input-tips {
visibility: visible;
}
当输入框获得聚焦时,伪类:focus
生效,触发第二个样式描述,提示文字显示。
(2)CSS属性选择器实现搜索过滤效果,如通讯录,城市列表等下拉菜单类型。我们只需要在输入内容时动态穿件一段css代码就可实现匹配,如下:
<input type="search" placeholder="输入城市名称或拼音" />
<ul>
<li data-search="重庆市 chongqing">重庆市</li>
<li data-search="哈尔滨 haerbing">哈尔滨市</li>
<li data-search="长春市 changchun">长春市</li>
</ul>
let eleStyle = document.createElement('style');
document.head.appendChild(eleStyle)
// 用户输入关键字
input.addEventListener("input", function() {
let value = this.value.trim()
eleStyle.innerHTML = value ? '[data-search]:not([data-search*="' + value + '"]) { display: none; }' : ''
})
利用属性选择器[attr *= 'val']
的包含特性,隐藏不需要显示的节点数据。
# JS获取渲染后的CSS误区
在JavaScript中,对后代选择符可能会有错误的认识。阅读本案例,笔者希望你能联想对比一下Vue项目中实现局部样式的方案。
可能你不知道笔者在说什么。直接入手实例,看看如下案例中的输出结果是多少?
<div id="ps">
<div class="boys">男孩</div>
<div class="girls">
<div class="lucy">露西</div>
</div>
</div>
// 长度是?
document.querySelectorAll('#ps div div').length;
// 长度是?
document.querySelector('#ps').querySelectorAll('div div').length;
有人会人为两者输出都是 1
。用JSbin试试,会发现第一个输出 1
,第二个输出 3
,为什么?
第一个符合预期,不解释。第二个为什么是3
?其实是因为无论样式在DOM中是渲染前还是渲染后,CSS选择器都是独立于整个页面的,具有全局特性。这就是为什么在一个DOM结构很深的页面上,你写个div div
,所有符合的子元素都会被选中的原因。这个道理,在JS中同样成立。
querySelectorAll
里面的选择器同样也具有全局特性。上文第二个案例解释过来就是:查询#myId
元素的子元素,选择所有整个页面下满足div div
选择器关系的DOM元素。
所以,在全局视野下,我们可以计算出 div.boys
、div.girls
、div.lucy
三者都满足以上描述。因此输出结果3
。
当然,也可以设置后面的选择器不是全局匹配的,加上伪类:scope
即可,修改如下:
document.querySelector('#ps').querySelectorAll(':scope div div').length;
// 哎呀妈呀,可算是1了
# 结束语
CSS还有很多好用的伪类,这里不一一做介绍,有想要了解的欢迎交流