🧼

DOMDOMタイムス#14: Eventインタフェースのbubblingとcomposed

2023/11/27に公開

めちゃくちゃ久しぶりの更新になってしまったドムドムタイムスです👶
(JSConfに全身全霊を捧げていました!)

さて、今日はEventインターフェースのbubblingcomposedについてチラチラ見ていってみます。

イベントのインターフェースのおさらい

まずイベントがどういうインターフェースになっていたか軽く確認してみましょう!

例えばclickイベントはPointerEventインターフェースを持ったオブジェクトの1つでした。
このPointerEventインターフェースとやらは辿っていってみると、下のようにEventインターフェースをinheritしているわけですね。

Event <- UIEvent <- MouseEvent <- PointerEvent

Eventインターフェースを起点として、いろいろなイベントが実装されています。

例えばEventインターフェースをinheritしているUIEventと同じレベルにSubmitEventCustomEventなどがあったり、マニアックなものだとGamepadEventなんかもあったり。もちろんもっとたくさんあります。

また、UIEventをinheritしているものとなると、その名の通りでUIに関わるイベントがてんこもりです🌞
MouseEventからKeyboardEventInputEventTouchEventなんかも。

気になる人はMDNのこのページからディグってみよう!
https://developer.mozilla.org/en-US/docs/Web/API/Event

Eventインターフェースのbubblingcomposed

Eventインターフェースが持つbubblingcomposedなる2つのプロパティが今日のテーマです👀
これらプロパティはそれぞれ下記を定義していて、その値は各イベントごとに決められているわけヨネ。もちろんCustomEventなんかは自分で決められますが👶

  • bubbling: trueならバブリングする。falseならしない
  • composed: trueならイベントはバブリング/キャプチャリングのときにshadow rootを超える。falseならshadow root止まり。

イベントの代表例をいくつかマッピングしてみると、以下のようになります。

bubbling: true bubbling: false
composed: true Aグループ(例: click(*1)) Bグループ(例: focus, blur)
composed: false Cグループ(例: mutationEvent(*2)) Dグループ(例: mouseenter, mouseleave)

*1: なんだかんだほとんどのUI Eventがここ
*2: documentが絡むもの(e.g.DOMNodeInsertedIntoDocument)は当てはまらない

で、ちょっと分かりやすくなるようにイベントの流れを図にしてみるとこんな感じ(ただしevent targetはshadow root外からだとshadow rootを持つ要素に書き換えられちゃうことに注意!!)。
window.topとshadow rootとevent targetが順に入れ子になっている四角系に矢印が4つ書いてある図。それぞれの矢印が表中のAからDと対応している。Aと書かれた矢印はwindow.topからshadow rootを貫通してevent targetまで伸びて、そのまままたwindow.topまで返ってきている。Bはwindow.topからshadow rootを貫通してevent targetまで伸びて終わり。Cはshadow rootからevent targetに伸びてshadow rootに返ってきている。Dはshadow rootからevent targetに伸びて終わり。

こうだろ〜!と思ってると意外と違う、なんてことがDOMを触っていると(自分は)あります🐤
変な動きしてるなと思ったときは、このあたりを疑ってみるのもいいかもしれません。

意外だったイベント

自分が初めて見たとき、なんで!?ってなったものを2つ書いておいてみます。

scrollイベント

1つめはscrollイベントがバブリングしないという有名な話です🌞
自分は最初これを知らずにめちゃくちゃハマりました。
というか、そもそもscrollbubblingcomposedはどこに仕様が載っているのでしょうか……?分かる方いたら教えてください😭

ちなみにChrome(v119)でやってみたところ、target要素でだけはバブリングフェーズでも反応しているっぽいですね。Firefox(v119)ではそんなことなさそうですが……汗

Chrome(v119) : window(capturing) -> target(capturing) -> target(bubbling) -> おわり
Firefox(v119): window(capturing) -> target(capturing) -> おわり

一方で、少なくともChrome(v119)やFirefox(v119)においては、少なくともdocumentがターゲットなら、scrollイベントのbubblingプロパティはtrueですバブリングします
でも、windowオブジェクトのすぐ下なので「バブリングしていると言ってもねえ……」という感が自分にはありますが👶

とりあえず色々と「なんで!?」感あるので、ここにノミネートされています。とりあえず仕様を見つけたいところです。

inputイベントとchangeイベント

inputイベントとchangeイベントも若干クセがあります。
両者ともに同じようなイベントに見えますがcomposedの値が違うわけですね。inputtruechangefalseです。

ちなみにこれはwhatwgでも議論になっています。歴史的背景、という感じなので興味がある人は読んでみて下さい!
https://github.com/whatwg/html/issues/5453

まとめ

イベントごとに異なるbubblingcomposedはたまにハマりどころになる印象があります。
前にFloating UI(旧Popper)のデバッグをしているときかなんかに、それこそscrollのバブリングでハマった覚えがあります。
こりゃ大変ですわ……😭

GitHubで編集を提案

Discussion