Chromeに実装された疑似クラス「:has()」がjQueryの「:has()」に悪影響、一定の条件下でWebサイトが壊れる可能性 - Publickey を読んだので、調べたものとあわせてまとめる。
三行
原因
- jQuery には CSS セレクタの拡張がされている
- jQuery Extensions | jQuery API Documentation
:has()
もそのひとつだった
- jQuery は、セレクタによって要素を取得する際、パフォーマンスのためにブラウザの
querySelectorAll()
メソッドを使う- セレクタが不正だった場合は
querySelectorAll()
メソッドがエラーになるので、 jQuery の拡張された実装で要素を取得し直す (フォールバックするような感じ)- (そのために jQuery の拡張を使わないほうが速いと書かれているドキュメントもある Optimize Selectors | jQuery Learning Center)
- セレクタが不正だった場合は
- かつては
:has()
はブラウザのquerySelectorAll()
メソッドにとって不正なセレクタだったため、 (エラーになるので、) jQuery の実装で要素が取得されていた- このほどブラウザで
:has()
が実装された- ここまでは問題なく、 jQuery でやっていたことがブラウザでもできるようになったということ
- ただし、
:has()
の引数に書かれたセレクタは不正でも無視される- forgiving-relative-selector-list というもの (後述) https://w3c.github.io/csswg-drafts/selectors-4/#typedef-forgiving-selector-list
:has()
の引数に jQuery の拡張のセレクタを使った場合、 jQuery では問題ないがブラウザのquerySelectorAll()
ではエラーになってほしいのだけど、エラーにはならない (単に取得するものがなかった扱いで成功する)
- このほどブラウザで
- これにより、
:has()
の引数に、ブラウザでは対応していない jQuery の拡張のセレクタを書いた場合、 jQuery で処理されなくなってしまった- 今まで jQuery で取得できていたが、取得できなくなった
forgiving-selector-list
MDN から :has()
の仕様をたどるとここで、引数には <forgiving-relative-selector-list>
をとると書かれている。
The relational pseudo-class, :has(), is a functional pseudo-class taking a <forgiving-relative-selector-list> as an argument.
https://w3c.github.io/csswg-drafts/selectors-4/#has-pseudo
<forgiving-relative-selector-list>
の項 https://w3c.github.io/csswg-drafts/selectors-4/#typedef-forgiving-relative-selector-list を見ると、 <forgiving-selector-list>
でありながら <complex-selector>
ではなく <relative-selector>
としてパースされるものとある。今回は <forgiving-selector-list>
であるという部分だけ気にしたらよい。
<forgiving-selector-list>
は、セレクタそれぞれをひとつずつパースし、パースに失敗したセレクタは無視して、成功したものだけを使うとのこと。今回の (ブラウザの) :has()
で querySelectorAll()
だけを試すとこういう感じで、ブラウザではセレクタとしてパースできない :even
が引数にあるが、エラーにならずに単に無視されている *1。
Chrome と Safari の状況の違い
- 記事中のツイートでは、 Safari の実装では jQuery の挙動を変えないと書かれているように読めた
- Safari は
:has()
の引数を forgiving-relative-selector-list として完全に実装できていないので、 jQuery の挙動が変わってしまう場合と変わらない場合がある- https://github.com/w3c/csswg-drafts/issues/6952#issuecomment-1148856670
:has()
の引数に、ブラウザではパースできないセレクタのみが渡された場合はエラーになるので、ブラウザのquerySelectorAll()
が従来通りエラーになるため、 jQuery の挙動は変わらない (jQuery の実装で要素を取得できる)- これは Safari が仕様に沿えていない (forgiving-relative-selector-list に完全に対応できていない) 状態 244708 – :has() selector does not accept forgiving selector list
:has()
の引数に、ブラウザでパースできるセレクタがひとつでも渡されていた場合、 forgiving-relative-selector-list の挙動になり、ブラウザのquerySelectorAll()
はエラーにならないため、 (Chrome 同様) jQuery の挙動は変わってしまう
実際 Safari で querySelectorAll()
だけを試すとそのようになっている *2。 :even
はブラウザでパースできないので、 :even
がついたセレクタだけが引数にある場合 (1行目と 3行目) はエラーになっている (この場合は jQuery が要素を取得でき、挙動は変わらない)。対して 2行目は body
がパースできるセレクタのため、エラーにならない (この場合は jQuery が処理をしないので、挙動が変わってしまう)。
されている対応と今後
- Chrome は Safari の実装に合わせた修正を入れた
- https://bugs.chromium.org/p/chromium/issues/detail?id=1358953
- Safari と同じ挙動なので、パースできるセレクタが含まれる
:has()
はエラーにはならない (のでその場合は jQuery の挙動は以前から変わっている状態)- が、そういう場合はあまりなかろうということで進んでいそう
Of course, there's still some breakage for selectors like `div:has(div, span:contains('Item'))` but, as I understand, Chrome is hoping that there's not much usage of it in the wild considering WebKit was able to go away with it.
https://bugs.chromium.org/p/chromium/issues/detail?id=1358953#c40
- W3C の対応 issue では、今はかなり混乱する状態で、ここから従来の jQuery の挙動を変えないようにしていくのがよかろう、と書かれていそう
- https://github.com/w3c/csswg-drafts/issues/7676#issuecomment-1250070779
- ブラウザの
:has()
の名前を変えて jQuery の:has()
に影響しないようにするか、ブラウザの:has()
の引数にパースできないセレクタが入っていたらエラーになるようにするか
感想
jQuery 側を直したい気持ちになるけど、古いバージョンのまま残っているサイトを壊さないという観点ではそれは難しいので、ブラウザ側がなんとかして避けるしかないのが (いつもそうだけどやはり) 大変そう。 forgiving-selector-list は初めて知ったし forgiving/unforgiving という表現が使われるのはへ〜という感じだった。あと Chromium の issue で querySelectorAll()
が qSA
と略されて呼ばれているのが面白い。
*1:Google Chrome 105.0.5195.102