写点什么

Chrome 浏览器的更新导致 jQuery 反复发版,只因 :has() 这个伪类

  • 2023-04-14
    广东
  • 本文字数:2736 字

    阅读完需:约 9 分钟

Chrome 浏览器的更新导致 jQuery 反复发版,只因 :has() 这个伪类

最近 jQuery 似乎又“回光返照”了一下,窜稀式地连更了几个小版本。我仔细看了看,并没有新功能出现,不过还挺有意思的,也让我学习到了新的东西。简单来说,这几个版本的更新主要与 Chrome 引入新选择器产生的缺陷有关。那么谷歌浏览器是如何导致 jQuery 产生 Bug 的,又是为何在修复之后又引发了新 Bug 的,这还得从 :has 伪类说起。

关于 :has() 伪类

CSS 在很早前就存在子代和后代选择器,而父元素的选择器出于性能的考虑却迟迟没有浏览器能支持,虽然规范早已存在,但是真正支持它却是不久前才发生的事。


这个伪类通过把可容错相对选择器列表作为参数,提供了一种针对引用元素选择父元素或者先前的兄弟元素的方法,例如:


h1:has(+ p) { color: red; }
复制代码


这段 css 表示的是:选择一个 h1 标签,当它有一个相邻节点是 p 标签时,这个 h1 标签文字会显示为红色,蛮好理解的对吧。


该伪类在 2022 年由 Safari 浏览器首先开始支持,随后 Chrome 105 也启用了这个原生的 :has 伪类,那么这个新选择器的支持为什么会给 jQuery 带来 Bug 呢?

都是 Chrome 浏览器的锅

其实 jQuery 长期以来一直支持 :has 伪类,甚至扩展了 :contains 这样的 api 出现在选择器中,它是以错误抛出(try-catch)的形式来判断应该使用浏览器的 querySelectorAll 还是 Sizzle(jQuery 开源的一套 css 解析器)来匹配样式的。


例如 :has(:contains("Item")) 这种形式的选择器,在以前会视为错误,所以当触发异常抛出时 jQuery 就会使用自己的解析工具从而保证选择器能正常工作。


但是现在 Chrome 更新了浏览器的 :has 原生支持,且作为参数的是可容错相对选择器列表,于是上述这种形式的写法直接被忽略,并不会报错,按照 jQuery 原本的处理方式,这里不抛出异常的话就会直接使用原生选择器,而原生选择器接收到错误的参数又直接“罢工”,所以导致了 jQuery 所有可追溯到最早版本的 :has 选择器都被破坏了。


这里 jQuery 团队用了一个梗作为段落标题:“打破互联网的并非拉尔夫”(" It wasn’t Ralph that broke the internet." ——拉尔夫是迪士尼电影《无敌破坏王 2:大闹互联网》中的主角)我的理解是在说凡事都有两面性,新事物也许是把双刃剑的意思吧,属于是委婉表达 :has 是个好东西但 Chrome 处理得有问题,虽然 jQuery 出了 Bug 但宝宝真的冤啊!放在咱们中文互联网的语境大概就是 jQuery 对 Chrome 唱起了:


不过 Chrome 团队也很快实施了一种解决方法来修复以前的 jQuery 版本,而 Safari 因为对 :has() 的实现的处理方式略有不同,没有遇到同样的问题。(Chrome:好了别说了,要不你再重启一下电脑试试?应该就没问题了😋)

“允许的”并不代表就是“正确的”

上文提到了 :has 包含的参数是可容错的,遵循所谓“宽容解析”( forgiving parsing )原则,怎么理解呢?我们先来看看什么是“无效的选择器列表”,如下有一段 CSS 规则集:


h1 {  color: red;}h2:invalid{  color: red;}h3 {  color: red;}
复制代码


很明显,上面 :invalid 是错误的一个伪类,所以 h2 的那段样式将是无效的,但通常我们还会这么写:


h1, h2:invalid, h3 {  color: red;}
复制代码


请注意,这两段规则集只在完全正确时是等效的,而下面这段规则集里只要存在一个不正确,整个规则都将不会被解析,也就是说 h1h2 不会有样式被应用了。


聪明的你应该想到了,可容错选择器就是在这个背景下诞生的,以 :is 为例,将第二段规则集改为如下写法,就与第一段完全等效了:


:is(h1, h2:invalid, h3) {  color: red;}
复制代码


:is:where:has 这些伪类都是相似的,因为参数列表可容错,所以它允许你传入可能有错误或不支持的参数,并且不会因为错误而“中断”选择器。

修复 Bug 往往会引发另一个 Bug

起初 jQuery 团队想到的修补方案,是通过判断包含了 :has 的选择器则强制使用 jQuery 的选择器引擎来解析,但这并不灵活,在社区的讨论声中大家普遍认为 jQuery 不应该依赖 try-catch,于是在 3.6.2 中 jQuery 开始改用 native 的策略 CSS.supports 来确定选择器是直接传递给 querySelectorAll 还是通过 jQuery 的选择器引擎。打住,这又是什么我没见过的黑魔法?吓得我赶紧翻了翻 MDN 文档。



CSS.supports() 有两种不同的传值形式。


第一种用来检验浏览器对于一对“属性 - 属性值”的支持,例如:


CSS.supports("display", "flex");CSS.supports('--foo', 'red');
复制代码


另一种传入一个包含检测条件的字符串:DOMString,例如:


CSS.supports('(--foo: red)');CSS.supports("( transform-origin: 5% 5% )");
复制代码


这种方法是支持选择器作为字符串来检测的,例如:


CSS.supports("selector(span)") // true
复制代码


这样就可以检测特定的伪类规则是否在浏览器中支持啦:



通过这个 supports("selector(SELECTOR)") 来确定一个选择器直接传递给 querySelectorAll 是否有效,返回 false 则退回到到自己的选择器引擎(Sizzle),这似乎是个理想的方案,不过 jQuery 团队明显对这个静态方法并不熟悉,所以在实现上犯了一点小错误,因为 SELECTOR 只支持 <complex-selector> 而不能是 <complex-selector-list>,例如:


CSS.supports("selector(div)"); // trueCSS.supports("selector(div, span)"); // false
复制代码


这意味着所有复杂的选择器列表都通过 Sizzle 解析而不是 querySelectorAll,所以 jQuery 又发了一个新版本 3.6.3 来修复这一问题。


但是,事情还没完,很快 jQuery 团队又发现这种实现方式仍然是有缺陷的😅(相关缺陷issues


有某些选择器在过去工作得很好,但 CSS.supports 却好像并不认为它们可以正常工作,例如原生的 querySelectorAll.("[attr=val") 是可以正常使用的,但无论是 CSS.supports( "selector(:is([attr=val))" ) 还是 CSS.supports( "selector([attr=val)" ) 都是返回 false.


我在当前浏览器 Chrome 110 中测试结果和官方不太一致,看来谷歌又偷偷改了点东西,根据 jQuery 官方的说法只有 Firefox 是相对稳定的。



在收到多条 issues 之后,jQuery 考虑恢复到以前的方式,同时,在 jQuery 与规范编写者和供应商的讨论中,一致认为需要防止类似 :has 的问题在未来再次发生,虽说收到一些“保证”,但考虑浏览器还需要一段时间去逐渐更新,所以在 3 月又发布了 3.6.4 这个版本并建议用户升级。


相关资料

jQuery 3.6.2 Released!

jQuery 3.6.3 Released: A Quick Selector Fix

jQuery 3.6.4 Released: Selector Forgiveness

MDN - CSS.supports()MDN - :has

Issue 1358953: :has pseudo-class breaks jQuery custom selectors


以上就是文章的全部内容,感谢看到这里!本人知识水平有限,如有错误望不吝指正,如果觉得写得不错,对你有所帮助或启发,可以点赞收藏支持一下,也欢迎关注,我会更新更多实用的前端知识与技巧。我是茶无味de一天(公众号: 品味前端),希望与你共同成长~

发布于: 刚刚阅读数: 3
用户头像

公众号:品味前端 2022-09-22 加入

一介前端,卖码为生。很惭愧,只希望在学习和分享的道路上能做一点微小的贡献。

评论

发布
暂无评论
Chrome 浏览器的更新导致 jQuery 反复发版,只因 :has() 这个伪类_CSS_茶无味的一天_InfoQ写作社区