01|WebKit 自己都承认:这件事在 CSS 世界里讨论了二十多年

:has() 真正扎人的地方,不是它终于来了。

而是它来得实在太晚。

WebKit 在 2022 年发布 :has() 支持说明时,开头几乎是摊牌式地回顾这段历史:

过去 二十多年,开发者反复去找 CSS Working Group,要求想办法给出一个“父选择器”。

需求很明确。

语法也不是想不出来。

真正卡住大家的,是另一句更冷的话:

浏览器得先搞清楚,面对可能非常复杂的循环模式,这东西到底能不能算得够快。

这一下,问题就不再是“为什么规范不体贴开发者”。

而变成了:

为什么一个大家想了二十年的直觉功能,会让浏览器引擎长期觉得自己背不起账。

这才是 :has() 最值得写的地方。


02|很多前端第一次看到它,心里冒出来的不是惊喜,而是怨气

这情绪太正常了。

因为 :has() 对前端来说,实在太像一个“早就该有”的能力。

你想根据子元素状态改父元素样式。

你想根据某个后代是否存在,决定当前容器怎么排版。

你想写出“如果里面有图片,就给外层另一套样式”“如果表单某处报错,就把整个块提亮”“如果卡片里出现某种内容,就让布局切换”的规则。

这些需求一点也不怪。

甚至可以说,它们太正常了。

正常到你会觉得:

CSS 怎么会拖到 2022 年,才终于让这类写法开始大规模进主流?

它不是“一个冷门能力终于发布”。

它是 CSS 世界里最著名的长期怨念之一,甚至可以说,是很多前端对“浏览器为什么总不肯给我顺手工具”这件事最集中的情绪投射。


03|表面上大家想要的是“父选择器”,实际上浏览器害怕的是“反向追责”

:has() 最常被大众理解成“父选择器终于来了”。

这句话传播上没问题。

但如果你只停在这层,就会低估浏览器为什么这么怕它。

因为 CSS 选择器系统长期有一种相对稳定的工作方式:

我去看这个元素自己长什么样,再看它和祖先、兄弟、状态之间的关系,然后决定它匹不匹配某条规则。

这套思路不是说永远简单。

但它大体上还是一种“我根据目标元素附近已知信息做匹配”的模型。

:has() 一进来,味道就变了。

因为它允许你写出这种逻辑:

这个元素自己的样式,要取决于它里面有没有某种后代、某种状态、某种结构。

一旦这样,浏览器面对的不再只是“当前元素该不该吃这条规则”。

它还得不断担心另一件事:

如果后代结构、状态、内容一变,我前面那一大串祖先是不是都可能得重新算?

这就是为什么很多实现者一提 :has(),脑子里想到的不是“语法很优雅”,而是“失效范围会不会炸开”。

说得更土一点:

以前 CSS 匹配很多时候像就地判断。

:has() 很容易把它变成连带责任。

你改一个孩子,可能要重查一串家长。

而浏览器最怕的,就是这种“看起来很顺手,但会把重算范围悄悄放大”的东西。


04|所以 :has() 难的从来不是“想不出来”,而是“你敢不敢发”

这是这条线最关键的一点。

很多人会误以为,:has() 迟到是因为工作组以前没想到。

不是。

恰恰相反。

这类想法在 CSS 世界里存在太久了,久到它已经不是什么新鲜创意,而是一种近乎执念的公共愿望。

真正卡住它的,从来都不是“语法不会写”。

而是:

  1. 浏览器担心选择器匹配成本失控
  2. 浏览器担心 DOM 变化时的样式失效和重计算太重
  3. 浏览器担心一旦大规模放进真实页面,性能问题会以非常难看的方式爆出来

这和很多功能的拖延都不一样。

有些 CSS 特性是因为需求不大。

有些是因为路线争议太强。

:has() 的尴尬在于,它偏偏是:

需求极真,直觉极强,但工程侧长期不敢拍胸口说“我扛得住”。

这就导致它在标准史里处于一种特别折磨人的状态:

  • 开发者永远在问“为什么还没有”
  • 浏览器永远在说“你先别急”
  • 双方都不是完全没道理

这类僵局,最容易一拖很多年。


05|这二十年的真正剧情,不是拒绝,而是长期不敢签收

如果你把 :has() 这条线写成“浏览器很保守,所以一直不肯给”,那就写浅了。

更准确的写法应该是:

浏览器并不是单纯反对这类能力,而是一直没有找到足够有把握的交付姿势。

这听起来像在替浏览器开脱。

但你把整个 Web 平台的历史放在一起看,就会发现这是个非常典型的模式。

浏览器团队真正怕的,从来不只是“做起来麻烦”。

他们更怕的是另一种事:

某个大家都很爱的能力,刚发货时像英雄,半年后却因为性能或兼容问题变成全网事故。

前缀时代已经证明过,一项半成熟能力只要足够受欢迎,就会被教程、框架、业务代码迅速固化成现实。

这时候你想回头再调,代价就会变得很难看。

所以 :has() 这么多年真正卡住的,不是“值不值得”。

而是:

谁能先站出来证明,这东西不只是优雅,而且跑得动。

没有这一步,它就会一直停在“人人想要,但谁也不敢先发”的区间里。


06|2022 年,WebKit 终于干了一件特别重要的事:不是宣布理想,而是宣布把账算平了

这就是为什么 Safari 15.4 的意义特别大。

WebKit 在 2022 年 3 月 发布关于 :has() 的文章时,传递出来的信息其实非常明确:

这不是“我们勉强放出来试试看”。

而更像是:

“我们已经把关键优化和实现路线想清楚了,所以现在可以认真把它交付给你。”

这一步很重要。

因为它让 :has() 这件事第一次从“多年愿望”变成“工程上可被证明的现实”。

更关键的是,WebKit 没有把这件事写成一句轻飘飘的“现在可以用了”。

它直接写到:团队为此做了 “novel :has-specific caching and filtering optimizations”,并结合了 CSS 引擎里已有的高级优化策略。

这句非常值钱。

因为它等于公开承认:

:has() 能上线,不是因为浏览器突然变勇敢了,而是因为终于有人把专门针对它的性能优化做出来了。

注意,这里真正有分量的,并不只是 Safari 抢先支持。

而是 Safari 作为第一个大规模主流实现,等于替整个行业做了一次证明:

这东西不是原则上不能做,而是需要有人把代价先啃下来。

这和很多标准史上的关键时刻都很像。

第一家真正把问题啃穿的,不只是发布了一个功能。

它还会顺带改写整个讨论气氛。

在这之前,大家谈的是:

“这会不会太贵?”

在这之后,大家谈的开始变成:

“既然有人已经把它做出来了,那别家什么时候跟?”

这就是立场变化。

一旦讨论从“能不能”切到“你什么时候上”,事情就已经不是原来那回事了。


07|Chrome 105 跟进的意义在于::has() 不再只是单引擎奇观

如果只有 WebKit 支持,:has() 仍然可以被看成一项漂亮但局部的胜利。

真正让它从“终于来了”升级成“前端心智真的开始改写”的,是 Chrome 1052022 年 8 月 的跟进。

这件事的意义在于:

开发者终于开始把它当成一项现实能力,而不是技术新闻。

而且 Chromium 这边给出的公开材料,也比“我们支持了”更硬。

在 Blink 的 Intent to Ship 讨论里,团队直接把担忧写成了工程条目:

  • :has() 可能增加 selector query 和 style invalidation 的复杂度
  • 为了压住风险,他们引入了单次操作级别的结果缓存
  • 目标是让处理时间接近 O(n)
  • 内存消耗接近 O(log n)
  • 还专门讨论了哪些嵌套写法会让 invalidation complexity 继续恶化

这让 :has() 的历史一下就很清楚了。

浏览器不是没看到它的价值。

浏览器是在等一整套“我怎么不被它拖死”的答案。

一项 CSS 特性从“新闻事件”变成“可写进项目”的能力,中间差的从来不只是一个浏览器。

差的是一种社会心理门槛。

只有当大家觉得:

  • 不再是孤例
  • 不再像实验
  • 不再只是某家浏览器的秀肌肉

它才会真正进入作者心智。

:has() 在 2022 年跨过去的,就是这道坎。

所以这条线最有意思的地方,不是“WebKit 赢了第一枪”“Chrome 很快跟上”这种赛马式叙事。

而是:

一个拖了二十年的公共愿望,终于从“原则上可想象”变成了“生态里可以认真依赖”。

这才是它真正的转折点。


08|为什么这件事特别能代表现代 CSS 的气质?

因为 :has() 很能说明,现代 CSS 里很多最受欢迎的能力,最后都不是靠“设计出语法”赢的。

而是靠“把性能恐惧处理到可接受”赢的。

这和外行对标准化的想象差别很大。

很多人以为标准化的难点在于概念争论、语法争论、委员会争论。

这些当然存在。

但到了 :has() 这种能力身上,你会发现真正拖时间的,往往是另一类更不显眼的东西:

  • 匹配策略
  • 失效范围
  • 动态状态变化
  • 大型页面下的最坏情况

这些都不容易写成戏剧化标题。

可它们恰恰决定了功能能不能活着进入生产世界。

这也是为什么 :has() 这条线特别有“江湖味”。

它表面上是一个选择器。

骨子里却是:

开发者直觉、规范愿望、引擎恐惧和工程耐心之间的一场长期拉扯。


09|如果你要给 :has() 下一个最准确的历史评价,它不是“终于补齐语法”,而是“终于有人敢为这份直觉买单”

:has() 不是那种靠愿景取胜的功能。

它能成功,正是因为它最后满足了现代 CSS 能活下来的那条铁律:

你不能只让作者觉得爽,你还得让浏览器觉得自己不会被拖死。

所以它这二十年最值得记住的,不是“前端等得好苦”。

而是:

浏览器花了二十年,才终于敢确认:这份大家都觉得理所当然的愿望,不会把平台压垮。

这是完全不同的视角。

也更接近标准史真正的残酷之处。

因为 Web 平台上很多被喊了很多年的需求,并不是死于没人喜欢。

它们往往死于:

没人愿意先为最坏情况签字。

:has() 的可贵,就在于它最后有人签了。


如果你只记一句,那就记这句:

:has() 难产二十年,不是因为 CSS 没想到“父选择器”,而是因为浏览器花了二十年,才终于敢确认自己背得动这笔性能账。


编者注(事实核对):has() 属于 Selectors Level 4 中的关系型伪类讨论范畴,长期因性能与实现顾虑被推迟。WebKit 在官方文章中明确写到:过去 twenty years,开发者反复要求“parent selector”,但浏览器一直担心 复杂循环模式 与足够快的计算速度。WebKit 于 2022-03Safari 15.4 公开发布 :has() 支持说明,并提到团队为此实现了 :has-specific caching and filtering optimizations;Chromium 于 2022-08Chrome 105 跟进,Blink 的 Intent to Ship 材料中也明确提到单次操作缓存、接近 O(n) 的处理时间与接近 O(log n) 的内存目标,以及对 invalidation complexity 的限制讨论。文中将其概括为“父选择器愿望”“性能恐惧被慢慢啃下来的能力”,属于写作性归纳,不是规范原文措辞。


关键人物速览

  • Jen Simmons:WebKit 阵营中把 :has() 推向大众前端视野的重要传播者之一。她让很多人第一次从实际用法而不是纯规范角度理解这项能力。
  • Tab Atkins Jr.:现代 CSS 模块与选择器讨论中的重要编辑人物,理解 :has() 所属的更大 CSS 演进脉络时绕不开他。
  • Igalia 团队:在现代 Web 平台一些艰难特性的跨引擎推进中经常出现。写 :has() 的工程落地时,了解这类实现推动力量有助于避免把故事写成单一厂商神话。

参考与延伸阅读

  1. Selectors Level 4
    https://www.w3.org/TR/selectors-4/

  2. WebKit:Using :has() as a CSS Parent Selector and much more
    https://webkit.org/blog/13096/css-has-pseudo-class/

  3. Chrome Developers::has() in Chrome 105
    https://developer.chrome.com/blog/has-m105/

  4. Blink-dev:Intent to Ship :has() pseudo class
    https://groups.google.com/a/chromium.org/g/blink-dev/c/bRsbl3wLuyk

  5. Igalia:How Blink tests :has() pseudo class
    https://blogs.igalia.com/blee/posts/2022/04/12/how-blink-tests-has-pseudo-class.html

  6. MDN::has()
    https://developer.mozilla.org/en-US/docs/Web/CSS/:has


下篇预告:为什么 container queries 这个看起来几乎天经地义的需求,会在很长一段时间里被不少人直接判成“不可能”。