OveUI博客
老前端的戎码生涯

浅谈逻辑选择器 is、where、not、has

在 CSS 选择器家族中,新增这样一类比较新的选择器 —逻辑选择器,目前共有 4 名成员:

  • :is
  • :where
  • :not
  • :has

本文将带领大家了解、深入它们。做到学以致用,写出更现代化的选择器。


:is 伪类选择器

:is()CSS伪类函数将选择器列表作为参数,并选择该列表中任意一个选择器可以选择的元素。

在之前,对于多个不同父容器的同个子元素的一些共性样式设置,可能会出现如下 CSS 代码:

headerp:hover,
mainp:hover,
footerp:hover{
color:red;
cursor:pointer;
}

而如今有了:is()伪类,上述代码可以改写成:

:is(header,main,footer)p:hover{
color:red;
cursor:pointer;
}

它并没有实现某种选择器的新功能,更像是一种语法糖,类似于 JavaScript ES6 中的 Class() 语法,只是对原有功能的重新封装设计,实现了更容易的表达一个操作的语法,简化了某些复杂代码的写法。

语法糖(syntactic sugar)是指编程语言中可以更容易的表达一个操作的语法,它可以使程序员更加容易去使用这门语言,操作可以变得更加清晰、方便,或者更加符合程序员的编程习惯。用比较通俗易懂的方式去理解就是,在之前的某个语法的基础上改变了一种写法,实现的功能相同,但是写法不同了,主要是为了让开发人员在使用过程中更方便易懂。

一图胜前言(引用至New CSS functional pseudo-class selectors :is() and :where()[1]):

图片

支持多层层叠连用

再来看看这种情况,原本的 CSS 代码如下:

<div><i>divi</i></div>
<p><i>pi</i></p>
<div><span>divspan</span></div>
<p><span>pspan</span></p>
<h1><span>h1span</span></h1>
<h1><i>h1i</i></h1>

如果要将上述 HTML 中,<div><p>下的<span><i>的 color 设置为 red,正常的 CSS 可能是这样:

divspan,
divi,
pspan,
pi{
color:red;
}

有了:is()后,代码可以简化为:

:is(div,p):is(span,i){
color:red;
}

结果如下:

图片

这里,也支持:is()的层叠连用。通过:is(div, p) :is(span, i)的排列组合,可以组合出上述 4 行的选择器,达到同样的效果。

当然,这个例子比较简单,看不出:is()的威力。下面这个例子就比较明显,这么一大段 CSS 选择器代码:

ololul,olulul,olmenuul,oldirul,
ololmenu,olulmenu,olmenumenu,oldirmenu,
ololdir,oluldir,olmenudir,oldirdir,
ulolul,ululul,ulmenuul,uldirul,
ulolmenu,ululmenu,ulmenumenu,uldirmenu,
uloldir,ululdir,ulmenudir,uldirdir,
menuolul,menuulul,menumenuul,menudirul,
menuolmenu,menuulmenu,menumenumenu,menudirmenu,
menuoldir,menuuldir,menumenudir,menudirdir,
dirolul,dirulul,dirmenuul,dirdirul,
dirolmenu,dirulmenu,dirmenumenu,dirdirmenu,
diroldir,diruldir,dirmenudir,dirdirdir{
list-style-type:square;
}

可以利用:is()优化为:

:is(ol,ul,menu,dir):is(ol,ul,menu,dir):is(ul,menu,dir){
list-style-type:square;
}

不支持伪元素

有个特例,不能用:is()来选取::before::after两个伪元素。譬如:

注意,仅仅是不支持伪元素,伪类,譬如:focus:hover是支持的。

divp::before,
divp::after{
content:"";
//...
}

不能写成:

divp:is(::before,::after){
content:"";
//...
}

:is选择器的优先级

看这样一种有意思的情况:

<div>
<pclass="test-class"id="test-id">where&istest</p>
</div>
<div>
<pclass="test-class">where&istest</p>
</div>

我们给带有.test-class的元素,设置一个默认的颜色:

div.test-class{
color:red;
}

如果,这个时候,我们引入:is()进行匹配:

div:is(p){
color:blue;
}

此时,由于div :is(p)可以看成div p,优先级是没有div .test-class高的,因此,被选中的文本的颜色是不会发生变化的。

但是,如果,我们在:is()选择器中,加上一个#test-id,情况就不一样了。

div:is(p,#text-id){
color:blue;
}

按照理解,如果把上述选择器拆分,上述代码可以拆分成:

divp{
color:blue;
}
div#text-id{
color:blue;
}

那么,我们有理由猜想,带有#text-id<p>元素由于有了更高优先级的选择器,颜色将会变成blue,而另外一个div p由于优先级不够高的问题,导致第一段文本依旧是green

但是,这里,神奇的是,两段文本都变成了blue

图片

CodePen Demo — the specificity of CSS :is selector[2]

这是由于,:is()的优先级是由它的选择器列表中优先级最高的选择器决定的。我们不能把它们割裂开来看。

对于div :is(p, #text-id)is:()内部有一个 id 选择器,因此,被该条规则匹配中的元素,全部都会应用div #id这一级别的选择器优先级。这里非常重要,再强调一下,对于:is()选择器的优先级,我们不能把它们割裂开来看,它们是一个整体,优先级取决于选择器列表中优先级最高的选择器

:is 的别名 :matches() 与 :any()

:is()是最新的规范命名,在之前,有过有同样功能的选择,分别是:

:is(div,p)span{}
//等同于
:-webkit-any(div,p)span{}
:-moz-any(div,p)span{}
:matches(div,p)span{}

当然,下面 3 个都已经废弃,不建议再继续使用。而到今天(2022-04-27):is()的兼容性已经非常不错了,不需要兼容 IE 系列的话可以考虑开始用起来(配合autoprefixer),看看CanIUse[3]

图片

:where 伪类选择器

了解了:is后,我们可以再来看看:where,它们两个有着非常强的关联性。:where同样是将选择器列表作为其参数,并选择可以由该列表中的选择器之一选择的任何元素。

还是这个例子:

:where(header,main,footer)p:hover{
color:red;
cursor:pointer;
}

上述的代码使用了:where,可以近似的看为:

headerp:hover,
mainp:hover,
footerp:hover{
color:red;
cursor:pointer;
}

这就有意思了,这不是和上面说的:is一样了么?

那么它们的区别在什么地方呢?

:is:where的区别

首先,从语法上,:is:where是一模一样的。它们的核心区别点在于优先级

来看这样一个例子:

<div>
<p>where&istest</p>
</div>

CSS 代码如下:

:is(div)p{
color:red;
}
:where(div)p{
color:green;
}

正常按我们的理解而言,:is(div) p:where(div) p都可以转化为div p,由于:where(div) p后定义,所以文字的颜色,应该是green绿色,但是,实际的颜色表现为color: red红色:

图片

这是因为,:where():is()的不同之处在于,:where()的优先级总是为 0,但是:is()的优先级是由它的选择器列表中优先级最高的选择器决定的。

上述的例子还不是特别明显,我们再稍微改造下:

<divid="container">
<p>where&istest</p>
</div>

我们给 div 添加上一个 id 属性,改造上述 CSS 代码:

:is(div)p{
color:red;
}
:where(#container)p{
color:green;
}

即便如此,由于:where(#container)的优先级为 0,因此文字的颜色,依旧为红色 red。:where()的优先级总是为 0这一点在使用的过程中需要牢记。

组合、嵌套

CSS 选择器的一个非常大的特点就在于组合嵌套。:is:where也不例外,因此,它们也可以互相组合嵌套使用,下述的 CSS 选择器都是合理的:

/*组合*/
:is(h1,h2):where(.test-a,.test-b){
text-transform:uppercase;
}
/*嵌套*/
.title:where(h1,h2,:is(.header,.footer)){
font-weight:bold;
}

这里简单总结下,:is:where都是非常好的分组逻辑选择器,唯一的区别在于:where()的优先级总是为 0,而:is()的优先级是由它的选择器列表中优先级最高的选择器决定的。

:not 伪类选择器

下面我们介绍一下非常有用的:not伪类选择器。

:not伪类选择器用来匹配不符合一组选择器的元素。由于它的作用是防止特定的元素被选中,它也被称为反选伪类(negation pseudo-class)。

举个例子,HTML 结构如下:

<divclass="a">div.a</div>
<divclass="b">div.b</div>
<divclass="c">div.c</div>
<divclass="d">div.d</div>
div:not(.b){
color:red;
}

div:not(.b)它可以选择除了 class 为.b元素之外的所有 div 元素:

图片

MDN 的错误例子?一个有意思的现象

有趣的是,在 MDN 介绍:not的页面,有这样一个例子:

/*SelectsanyelementthatisNOTaparagraph*/
:not(p){
color:blue;
}

意思是,:not(p)可以选择任何不是<p>标签的元素。然而,上面的 CSS 选择器,在如下的 HTML 结构,实测的结果不太对劲。

<p>p</p>
<div>div</div>
<span>span</span>
<h1>h1</h1>

结果如下:

图片

意思是,:not(p)仍然可以选中<p>元素。我尝试了多个浏览器,得到的效果都是一致的。

CodePen Demo — :not pesudo demo[4]

这是为什么呢?这是由于:not(p)同样能够选中<body>,那么<body>的 color 即变成了blue,由于color是一个可继承属性,<p>标签继承了<body>的 color 属性,导致看到的<p>也是蓝色。

我们把它改成一个不可继承的属性,试试看:

/*SelectsanyelementthatisNOTaparagraph*/
:not(p){
border:1pxsolid;
}
图片

OK,这次<p>没有边框体现,没有问题!实际使用的时候,需要注意这一层继承的问题!

:not 的优先级问题

下面是一些使用:not需要注意的问题。

:not:is:where这几个伪类不像其它伪类,它不会增加选择器的优先级。它的优先级即为它参数选择器的优先级。

并且,在CSS Selectors Level 3[5]:not()内只支持单个选择器,而从 CSS Selectors Level 4[6]开始,:not()内部支持多个选择器,像是这样:

/*CSSSelectorsLevel3,:not内部如果有多个值需要分开*/
p:not(:first-of-type):not(.special){
}
/*CSSSelectorsLevel4支持使用逗号分隔*/
p:not(:first-of-type,.special){
}

:is()类似,:not()选择器本身不会影响选择器的优先级,它的优先级是由它的选择器列表中优先级最高的选择器决定的。

:not(*) 问题

使用:not(*)将匹配任何非元素的元素,因此这个规则将永远不会被应用。

相当于一段没有任何意义的代码。

:not() 不能嵌套 :not()

禁止套娃。:not伪类不允许嵌套,这意味着:not(:not(...))是无效的。

:not() 实战解析

那么,:not() 有什么特别有意思的应用场景呢?我这里列举一个。

W3 CSS selectors-4 规范[7]中,新增了一个非常有意思的:focus-visible伪类。

:focus-visible这个选择器可以有效地根据用户的输入方式(鼠标 vs 键盘)展示不同形式的焦点。

有了这个伪类,就可以做到,当用户使用鼠标操作可聚焦元素时,不展示:focus样式或者让其表现较弱,而当用户使用键盘操作焦点时,利用:focus-visible,让可获焦元素获得一个较强的表现样式。

看个简单的 Demo:

<button>Test1</button>
button:active{
background:#eee;
}
button:focus{
outline:2pxsolidred;
}

使用鼠标点击:

图片

可以看到,使用鼠标点击的时候,触发了元素的:active伪类,也触发了:focus伪类,不太美观。但是如果设置了outline: none又会使键盘用户的体验非常糟糕。因为当键盘用户使用 Tab 尝试切换焦点的时候,会因为outline: none而无所适从。

因此,可以使用:focus-visible伪类改造一下:

button:active{
background:#eee;
}
button:focus{
outline:2pxsolidred;
}
button:focus:not(:focus-visible){
outline:none;
}

看看效果,分别是在鼠标点击 Button 和使用键盘控制焦点点击 Button:

图片

CodePen Demo — :focus-visible example[8]

可以看到,使用鼠标点击,不会触发:foucs,只有当键盘操作聚焦元素,使用 Tab 切换焦点时,outline: 2px solid red这段代码才会生效。

这样,我们就既保证了正常用户的点击体验,也保证了无法使用鼠标的用户的焦点管理体验,在可访问性方面下了功夫。

值得注意的是,这里为什么使用了button:focus:not(:focus-visible)这么绕的写法而不是直接这样写呢:

button:focus{
outline:unset;
}
button:focus-visible{
outline:2pxsolidred;
}

解释一下,button:focus:not(:focus-visible)的意思是,button 元素触发 focus 状态,并且不是通过 focus-visible 触发,理解过来就是在支持:focus-visible的浏览器,通过鼠标激活:focus的 button 元素,这种情况下,不需要设置outline

为的是兼容不支持:focus-visible的浏览器,当:focus-visible不兼容时,还是需要有:focus伪类的存在。

因此,这里借助:not()伪类,巧妙的实现了一个实用效果的方案降级。

这里有点绕,需要好好理解理解。

:not 兼容性

经历了 CSS Selectors Level 3 & CSS Selectors Level 4 两个版本,到今天(2020-05-04),除去 IE 系列,:not的兼容性已经非常之好了:

图片

:has 伪类选择器

OK。最后到所有逻辑选择器里面最重磅的:has出场了。它之所以重要是因为它的诞生,填补了在之前 CSS 选择器中,没有核心意义上真正的父选择器的空缺。

:has伪类接受一个选择器组作为参数,该参数相对于该元素的:scope[9]至少匹配一个元素。

实际看个例子:

<div>
<p>div--p</p>
</div>
<div>
<pclass="g-test-has">div--p.has</p>
</div>
<div>
<p>div--p</p>
</div>
div:has(.g-test-has){
border:1pxsolid#000;
}

我们通过div:has(.g-test-has)选择器,意思是,选择 div 下存在 class 为.g-test-has的 div 元素。

注意,这里选择的不是:has()内包裹的选择器选中的元素,而是使用:has()伪类的宿主元素。

效果如下:

图片

可以看到,由于第二个 div 下存在 class 为.g-test-has的元素,因此第二个 div 被加上了 border。

:has() 父选择器 — 嵌套结构的父元素选择

我们再通过几个 DEMO 加深下印象。:has()内还可以写的更为复杂一点。

<div>
<span>divspan</span>
</div>

<div>
<ul>
<li>
<h2><span>divullih2span</span></h2>
</li>
</ul>
</div>

<div>
<h2><span>divh2span</span></h2>
</div>
div:has(>h2>span){
margin-left:24px;
border:1pxsolid#000;
}

这里,要求准确选择 div 下直接子元素是 h2,且 h2 下直接子元素有 span 的 div 元素。注意,选择的最上层使用 :has() 的父元素 div。结果如下:

图片

这里体现的是嵌套结构,精确寻找对应的父元素

:has() 父选择器 — 同级结构的兄元素选择

还有一种情况,在之前也比较难处理,同级结构的兄元素选择。

看这个 DEMO:

<divclass="has-test">div+p</div>
<p>p</p>

<divclass="has-test">div+h1</div>
<h1>h1</h1>

<divclass="has-test">div+h2</div>
<h2>h2</h2>

<divclass="has-test">div+ul</div>
<ul>ul</ul>

我们想找到兄弟层级关系中,后面接了<h2>元素的 .has-test元素,可以这样写:

.has-test:has(+h2){
margin-left:24px;
border:1pxsolid#000;
}

效果如下:

图片

这里体现的是兄弟结构,精确寻找对应的前置兄元素

这样,一直以来,CSS 没有实现的父选择器,借由:has()开始,也能够做到了。这个选择器,能够极大程度的提升开发体验,解决之前需要比较多 JavaScript 代码才能够完成的事。

上述 DEMO 汇总,你可以戳这里CodePen Demo — :has Demo[10]

:has() 兼容性,给时间一点时间

比较可惜的是,:has()在最近的Selectors Level 4[11]规范中被确定,目前的兼容性还比较惨淡,截止至 2022-05-04,Safari 和 最新版的 Chrome(V101,可通过开启Experimental Web Platform features体验)

图片

Chrome 下开启该特性需要,1. 浏览器 URL 框输入 chrome://flags,2. 开启 #enable-experimental-web-platform-features

耐心等待,给给时间一点时间,这么好的选择器马上就能大规模应用了。

最后

本文到此结束,希望对你有帮助 🙂

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 —iCSS前端趣闻😄

更多精彩 CSS 技术文章汇总在我的Github — iCSS[12],持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

参考资料

[1]

New CSS functional pseudo-class selectors :is() and :where():https://web.dev/css-is-and-where/[2]

CodePen Demo — the specificity of CSS :is selector:https://codepen.io/Chokcoco/pen/rNJaGvb[3]

CanIUse:https://caniuse.com/?search=%3Amatches[4]

CodePen Demo — :not pesudo demo:https://codepen.io/Chokcoco/pen/KKZbWjy[5]

CSS Selectors Level 3:https://www.w3.org/TR/selectors-3/[6]

CSS Selectors Level 4:https://www.w3.org/TR/selectors-4/[7]

W3 CSS selectors-4 规范:https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo[8]

CodePen Demo — :focus-visible example:https://codepen.io/Chokcoco/pen/abBbPrE[9]

:scope:https://developer.mozilla.org/zh-CN/docs/Web/CSS/:scope[10]

CodePen Demo — :has Demo:https://codepen.io/Chokcoco/pen/poaJjwm[11]

Selectors Level 4:https://drafts.csswg.org/selectors/#relational[12]

Github — iCSS:https://github.com/chokcoco/iCSS

赞(0)
未经允许不得转载:UI树欲静 » 浅谈逻辑选择器 is、where、not、has