现代 CSS

CSS 的父选择器:has()

特别声明:如果您喜欢小站的内容,可以点击申请会员进行全站阅读。如果您对付费阅读有任何建议或想法,欢迎发送邮件至: airenliao@gmail.com!或添加QQ:874472854(^_^)

W3C 的 Selectors Level 4 新增了很多强大的 CSS 选择器。早在 2018 年年底就在《初探CSS 选择器Level 4》一文中和大家一起探讨了这些选择器。在这些新选择器中,最为有意思的是“逻辑组合选择器”,即 “任意匹配伪类选择器:is()、否定(匹配无)伪类选择器:not()、选择器权生调整伪类选择器:where()和关系性(父选择器)伪类选择器:has() 。尤其是关系性伪类选择器:has(),它和 CSS 容器查询在近十多年来一直成为 Web 开发者期待的 CSS 功能之一。

在这篇文章中,我将和大家一起来探讨什么是关系性伪类选择器(又称父选择器)以及它是如何工作的,并且将会通过一些示例来阐述该选择器可以在哪里,最重要的是我们现在如何使用它。

曾在《初探CSS 选择器Level 4》和《CSS 选择器:is():where():has() 有什么功能》两篇博文中介绍过新增的逻辑组合选择器,感兴趣的可以先一睹为快!

简单聊一下 CSS 选择器

稍微了解 CSS 的同学都知道,要想给页面添加样式,就得使用 CSS 选择器 来选中 DOM 元素,否则添加的样式就无法运用到具体的元素上。选择器相关的知识是 CSS 领域最基础的部分,但涉及选择器的知识也很多,这一点从 W3C 有关于 选择器规范版本迭代的变更中不难发现(现在已更新到第四版本了,即 Selectors Level 4)。

社区中有关于 CSS 选择器介绍的文章也很多,我个人比较喜欢 nana (@nanacodesign)的 《CSS selectors cheatsheet & details》一文,她用图文并茂的方式介绍了 CSS 选择器常见类型:

国内 @张鑫旭 老师有一本专门介绍 CSS 选择器的书

小站上也陆续也发布了一些关于 CSS 选择器的文章,摘出一些基础和有意思的文章供大家参考:

图解CSS》系列中有关于 CSS 选择器这一章节正在编写之中,感兴趣的可以关注后续相关更新!

CSS选择器简单,而且种类繁多,但一直以来开发者都希望有一个能选中父选择器类型。那么什么是父选择器呢?感兴趣的请继续往下阅读。

父选择器:has()是什么?

用一句话来描述: :has()选择器是一个关系型伪类选择器,也被称为函数型伪类选择器,它和 :is():not() 以及 :where()函数型选择器被称为 CSS的逻辑组合选择器

注意,在规范中并没有父选择器一描述,社区中把:has()描述为“父选择器”是因为这样更形象,也易于理解。

我们先从其他选择器着手来介绍父选择器是什么?

正如 nana 提供的选择器示例所示,CSS 选择器中有很多类型的选择器是和 DOM 结构有关的,比如我们熟悉的 子选择器(a > b后代选择器(a b相邻兄弟选择器(a + b通用兄弟选择器(a ~ b结构伪类选择器(比如 :nth-child():nth-of-type()等)

但在这众多的 CSS 选择器中就没有“父选择器”,或许也正因为他的缺失,很多开发者都希望有这样的一个选择器,即,能通过子元素选中其父元素。事实上,社区的开发者从未停止过这方面的探索。比如 Shaun Inman (@shauninman)早在2008年就提出了父选择器的语法规则,即 E < F 。这个语法规则看上去和CSS的子选择器有点相似,只是符号从 > 变成 <了。

<!-- HTML -->
<a href="##">
    <img src="" alt="" />
</a>

/* 子选择器 E > F */
a > img {
    border: 2px solid #09f; /* 选中的是子元素 img */
}

/* 父选择器 E < F */
a < img {
    border: 2px solid #09f; /* 选中的是父元素 a */
}

之后 Remy Sharp(@rem)建议使用一个 :parent 伪元素来表述父选择器的语法

a img:parent { 
    border: 2px solid #09f; 
}

稍微熟悉CSS选择器的开发者都知道,选择器最右位才是主体,你要样式化的东西(元素)。大多数编写CSS的人,在某种程度上,发现自己想要基于其中的内容来设计样式。按照这个规则来理解的话,@shauninman 和 @rem 提出的父选择器语法规则都超出我们的认知,比如E < F选择器的主体是左侧的E

后来,Igalia公司的工程师和浏览器内核的工师提出使用 :has() 来定义父选择器的语法规则:

E:has(F) {

}

:has() 选择器看上去和 jQuery 中的:has()选择器相似。

我们再来看一下W3C规范是怎么描述:has()选择器

The relational pseudo-class, :has(), is a functional pseudo-class taking a <forgiving-relative-selector-list> as an argument. It represents an element if any of the relative selectors, when absolutized and evaluated with the element as the :scope elements, would match at least one element.

大致意思是:

关系型伪类:has()是一个函数型伪类,接受一个选择器组(< forgive -relative-selector-list>)作为参数。其给定的选择器参数(相对于该元素的:scope)至少匹配一个元素。

其实,我们可以像理解jQuery中的:has() 选择器那样来理解:

:has()选择器选取所有包含一个或多个元素在其内的元素,匹配指定的选择器

即,:has()选择器接受一个相对的选择器列表,如果至少有一个其他元素与列表中的选择器相匹配,那么它将代表一个元素。如果这样不好理解,可以通过下面的示例来理解。假设在我们的 HTML 中有两段这样的代码:

<!-- ① 含有卡片缩略图 img -->
<div class="card">
    <img class="card__thumb" src="" alt="" />
    <div class="card__content">
        <h3 class="card__title">Card Title</h3>
        <p class="card__describe">Card Describe</p>
    </div>
</div>

<!-- ② 不含卡片缩略图 img -->
<div class="card">
    <div class="card__content">
        <h3 class="card__title">Card Title</h3>
        <p class="card__describe">Card Describe</p>
    </div>
</div>

① 和 ② 唯一的区别就是,在 ② 代码片段中不包含卡片缩略图 img。如果我们在CSS中使用像下面这段CSS代码:

.card {
    border-radius: 0.5rem;
    box-shadow: 0 0.25rem 0.5rem -0.15rem hsla(0 0% 0% / 55%);
    background-color: #fff;
    padding: 1em 2em;
    min-width: 320px;
}

.card:has(img) {
    display: flex;
    align-items: center;
    gap: 1em;
    padding: 0 2em 0 0;
}

在支持:has() 浏览器(写这篇文章为止,你可以在 Safari 15.4 或 Chrome Canary 最新版本)中看到下图这样的效果:

其中:

.card:has(img) {
    display: flex;
    align-items: center;
    gap: 1em;
    padding: 0 2em 0 0;
}

上面这段代码表示的是,含有 img.card 元素重置了 .cardpadding 值,并且添加了 Flexbox 布局相关的样式。换句话说,.card:has(img) 选择器的意思相当于 .card元素中包含了img元素吗? 简单地说,在CSS的选择器中有了一个条件判断的逻辑:

if (.card元素包含了img元素) {
    .card {
        display: flex;
        align-items: center;
        gap: 1em;
        padding: 0 2em 0 0;
    }
} else {
    .card {
        padding: 1em 2em;
    }
}

很神奇吧!

父选择器为何会缺失这么久?

父选择器和容器查询特性在近十多年来一直都是众多Web开发者所期待的特性,如果你一直有关注 CSS 状态发展相关的报告,不难发现父选择器和容器查询特性都一直排列前列:

既然“父选择器”众人期待,而且又是那么实用,怎么在 CSS 选择器中一直就没有“父选择器”呢?

正如 Jonathan Snook(@snookca)在2010年的一篇文章中描述的那样,这不仅是因为性能方面的考虑,还因为浏览器引擎渲染文档并将计算样式应用于元素的方式:作为一个流,一个元素接一个元素进入

上面视频来自于 Ponime 在 YouTube 上发布的视频:Gecko Reflow Visualization - mozilla.org

正如上面视频所演示的那样,当一个元素在浏览器屏幕上渲染出来时,它的父元素以及父元素渲染好的UI样式都已经在那里了。在此之后,重新绘制父节点(以及所有其他父节点)将需要对所有父节点选择器进行另一计算。这样的计算对渲染引擎来说是昂贵的!

曾在《初探 CSS 渲染引擎》和《理解 Web 的重排和重绘》两篇博文中有涉猎过这方面的知识。

之前在整理有关于CSS选择器对性能影响文章(《编写高效 CSS 选择器》)时,发现CSS的通用选择器(*)是效率最低的CSS选择器。也正如乔纳森.斯努克(Jonathan Snook)说,

如果有一个父选择器,那将很容易成为低效率选择器中的新老大。

其理由是,当从页面中动态地添加和删除元素时,可能会导致整个文档需要重新渲染(主要是内存使用方面的问题),即很容易产生重绘和重排。即使如此,乔纳森.斯努克(Jonathan Snook)仍然很乐观:

我所描述的在技术上并非不可能。事实上,恰恰相反。这只是意味着我们必须处理使用这种特性所带来的性能影响。

这观点后来也得到了 Eric Meyer(@meyerweb)印证,“性能问题可能已经解决了”!Eric Meyer 在 Twitter上发了一条信息,提到了如何避免一直困扰着父选择器给渲染带来的性能问题

如果对于该话题感兴趣的话,可以直接观看 Byungwoo Lee(@byungwoo_bw)在YouTube发的视频《'has' prototyping status》,视频中对应的 PPT 可以点击这里查阅。Byungwoo Lee 还专门用了两篇文章(《CSS Selectors :has():A way of selecting the parent element》和《How blink tests :has() pseudo class:How to use cache to control :has() complexity》)介绍 :has()的使用、存在问题以及如何使用缓存来控制:has()的复杂性等。

简而言之,浏览器渲染引擎的策略就好像是下象棋一要,应该快速找到如何忽略无关的走法,而不是预测每种走法组合的所有可能结果。对于 CSS 而言,渲染引擎会防止对不相关元素的计算。为了减少应用样式后不相关的重新计算,渲染引擎可以在重新计算期间标记样式是否受到:has()状态更改的影响。

另外,这几年浏览器的渲染引擎已经有了相当大的改进。渲染过程已经被优化到浏览器可以有效地确定哪些需要渲染或更新,哪些不需要,从而为一系列新的和令人兴奋的功能开辟了道路。正因为这些的改进,有机会让 :has() 看到曙光。到目前为止,你可以在 Safari 15.4+ 和 Chrome Canary(写这篇文章时是 103.0.5011.0 版本)可以看到 :has() 效果。同时也希望Firefox和Chrome也能快速跟上。

说个题外话,最早实现 :has() 选择器的浏览是 Safari,正如 Jen Simmons(@jensimmons)在 Twitter 所言:“不要老说Safari总是最后一个。有时我们是第一”。

自从Jen Simmons加入Safari 的Web开发者体验团队之后,Webkit内核在CSS特性上的更新速度较

剩余80%内容付费后可查看
返回顶部