前端到底是“死”没死我是不知道,但是 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 但宝宝真的冤啊!
不过 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;
}
请注意,这两段规则集只在完全正确时是等效的,而下面这段规则集里只要存在一个不正确,整个规则都将不会被解析,也就是说 h1
和 h2
不会有样式被应用了。
聪明的你应该想到了,可容错选择器就是在这个背景下诞生的,以 :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)"); // true
CSS.supports("selector(div, span)"); // false
这意味着所有复杂的选择器列表都通过 Sizzle 解析而不是 querySelectorAll
,所以
jQuery 又发了一个新版本 3.6.3 来修复这一问题。
但是,事情还没完,很快 jQuery 又发现这种实现方式仍然是有缺陷的😅
有某些选择器在过去工作得很好,但 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 这个版本并建议用户升级。