要約
DOMイベントはその名の通りDOMの構造に従って伝搬する。
バブリング/キャプチャリングをイメージできるかがカギ。
見た目に騙されちゃダメ
1. はじめに
まずはじめに「へんな動きをするデモ」を紹介します。
次の2つは "四角形の中ではマウスカーソルを非表示にして代わりに円を追従させる" ことを実現しようとしたコードです。
- デモ①
See the Pen mouse-cursor by www-tacos (@www-tacos) on CodePen.
- デモ②
See the Pen mouse-cursor-parallel by www-tacos (@www-tacos) on CodePen.
いくらか触ってもらえれば気づくと思いますが、
-
デモ①はゆっくり動かすと円が四角形の外に出てしまう
-
デモ②はマウスカーソルの表示/非表示が交互に切り替わって点滅してるみたいになる
という動きをします。
なぜこうなるのか?そこにはHTMLの構造とイベント伝搬の仕組みが関わってきます。
2. DOMツリー
HTMLは要素が入れ子構造をつくるため木構造で表現することができ、実際に木構造に変換したデータを "DOMツリー" といったりします。
例えばデモ①のHTMLはdiv#square
の中にdiv#circle
があるので次のようなDOMツリーと考えられます。
一方でデモ②のHTMLはdiv#square
とdiv#circle
が並んでいるので次のようなDOMツリーと考えられます。
このような違いがあると、例えばdiv#circle
に width: 80%
を指定した場合
-
デモ①は
div#square
の横幅の80%となる -
デモ②はbodyの横幅の80%となる
という違いが生じます。
またDOMツリーの違いはイベント伝搬にも影響し、今回はこちらが重要になってきます。
3. バブリング/キャプチャリング
イベントはイベントハンドラを設定した要素でのみ発生するものと思っていましたが、実はそうではなく、親要素/自要素/子要素間で伝搬する仕組みになっています。
通常は、ある要素でイベントが発生するとまずその要素のイベントハンドラーが実行され、次にその要素の親要素のイベントハンドラーが実行されます。
このように親要素に伝搬していく流れを "バブリング" といいます。(泡が昇っていく様になぞらえて)
一方で、ある要素でイベントが発生したときにその一番遠い親要素のイベントハンドラーから実行していく流れもあり、これを "キャプチャリング" といいます。
以下は各デモのDOMツリーに対してキャプチャリングの流れとバブリングの流れを追記した図です。
- デモ①
- デモ②
バブリングとキャプチャリングはどちらか片方しか取り得ないわけではなく、すべてのイベントについてキャプチャリングフェーズとバブリングフェーズがあり、どのフェーズでイベントハンドラを実行するかを自分で決めることができます。
// addEventListenerの第3引数にキャプチャをONにする設定値を追加する
div.addEventListener("click", (e) => { console.log("clicked") }, { capture: true });
例えばデモ①でいうと、div#square
にキャプチャリングフェーズで動くイベントハンドラAとバブリングフェーズで動くイベントハンドラBを設定していて、div#circle
で目的のイベントが発生した場合、
- キャプチャリングフェーズを
body
→div#square
→div#circle
と進めていき、その途中でdiv#square
に設定されたイベントハンドラAを実行する -
div#circle
まで到達してキャプチャリングフェーズ終了(バブリングフェーズに移行) - バブリングフェーズを
div#circle
→div#square
→body
と進めていき、途中でdiv#square
に設定されたイベントハンドラBを実行する
のようなイベント伝搬(処理順)となります。
また、イベントの伝搬はDOM構造に依存していてレイアウトは関係ないですが、イベントの発火はマウスが重なる要素で起こるのでレイアウトが関係してきます。
どこでイベントが発生するかは見た目通りなのにどの要素にイベントが伝搬するかは見た目とは関係ないというのはややこしいですね。
デモの改善
さて、これまでを踏まえて最初のデモがへんな動きをした理由を考えてみます。
- デモ①
div#square
の中でマウスを動かすと常にマウスの下に div#circle
が表示され、div#circle
に対して発生した mousemove
が div#square
に伝搬してしまうため、マウスが div#square
の外に出ても div#circle
の表示が続いてしまった。
- デモ②
div#square
の中でマウスを動かすと常にマウスの下に div#circle
が表示され、div#circle
が div#square
での mousemove
イベントの発生を妨げてしまうため、マウスが div#circle
のうえにいる間はマウスカーソルが表示されてしまった。
つまり両方ともマウスカーソルの下に現れる div#circle
がイベントの発生対象であることが原因だとわかります。
そこで、スタイルシートで div#circle
に pointer-events: none
を指定してみます。
これを指定した要素はポインターイベントの対象外となるため、デモ①については mousemove
イベントが発生しなくなり、デモ②については div#square
の mousemove
イベントが発生するようになります。
4. さいごに
イベント伝搬を完全に理解したので次はキャプチャリングを活かしたデザインとか考えてみたいと思います^^
記事は以上になります。最後まで読んでくださりありがとうございます。