hogashi.*

日記から何から

ブラウザで:has()セレクタが実装されてjQueryの:has()セレクタの挙動が変わったの調べた

 Chromeに実装された疑似クラス「:has()」がjQueryの「:has()」に悪影響、一定の条件下でWebサイトが壊れる可能性 - Publickey を読んだので、調べたものとあわせてまとめる。

三行

原因

  • jQuery には CSS セレクタの拡張がされている
  • jQuery は、セレクタによって要素を取得する際、パフォーマンスのためにブラウザの querySelectorAll() メソッドを使う
    • セレクタが不正だった場合は querySelectorAll() メソッドがエラーになるので、 jQuery の拡張された実装で要素を取得し直す (フォールバックするような感じ)
  • かつては :has() はブラウザの querySelectorAll() メソッドにとって不正なセレクタだったため、 (エラーになるので、) jQuery の実装で要素が取得されていた
    • このほどブラウザで :has() が実装された
      • ここまでは問題なく、 jQuery でやっていたことがブラウザでもできるようになったということ
    • ただし、 :has() の引数に書かれたセレクタは不正でも無視される
  • これにより、 :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


ChromeSafari の状況の違い
  • 記事中のツイートでは、 Safari の実装では jQuery の挙動を変えないと書かれているように読めた
    • しかし調べたところでは、 Safari でも部分的に jQuery の挙動を変えてしまっていそうに見える
  • Safari:has() の引数を forgiving-relative-selector-list として完全に実装できていないので、 jQuery の挙動が変わってしまう場合と変わらない場合がある

 実際 SafariquerySelectorAll() だけを試すとそのようになっている *2:even はブラウザでパースできないので、 :even がついたセレクタだけが引数にある場合 (1行目と 3行目) はエラーになっている (この場合は jQuery が要素を取得でき、挙動は変わらない)。対して 2行目は body がパースできるセレクタのため、エラーにならない (この場合は 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

感想

 jQuery 側を直したい気持ちになるけど、古いバージョンのまま残っているサイトを壊さないという観点ではそれは難しいので、ブラウザ側がなんとかして避けるしかないのが (いつもそうだけどやはり) 大変そう。 forgiving-selector-list は初めて知ったし forgiving/unforgiving という表現が使われるのはへ〜という感じだった。あと Chromium の issue で querySelectorAll()qSA と略されて呼ばれているのが面白い。

*1:Google Chrome 105.0.5195.102

*2:Safari 16.0 (17614.1.25.9.10, 17614)