5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptAdvent Calendar 2023

Day 22

DOMイベントの伝わり方、完全に理解した

Last updated at Posted at 2023-12-21

要約

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#squarediv#circleが並んでいるので次のようなDOMツリーと考えられます。

このような違いがあると、例えばdiv#circlewidth: 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 で目的のイベントが発生した場合、

  1. キャプチャリングフェーズを bodydiv#squarediv#circle と進めていき、その途中で div#square に設定されたイベントハンドラAを実行する
  2. div#circle まで到達してキャプチャリングフェーズ終了(バブリングフェーズに移行)
  3. バブリングフェーズを div#circlediv#squarebody と進めていき、途中で div#square に設定されたイベントハンドラBを実行する

のようなイベント伝搬(処理順)となります。

また、イベントの伝搬はDOM構造に依存していてレイアウトは関係ないですが、イベントの発火はマウスが重なる要素で起こるのでレイアウトが関係してきます。
どこでイベントが発生するかは見た目通りなのにどの要素にイベントが伝搬するかは見た目とは関係ないというのはややこしいですね。

デモの改善

さて、これまでを踏まえて最初のデモがへんな動きをした理由を考えてみます。

  • デモ①

div#square の中でマウスを動かすと常にマウスの下に div#circle が表示され、div#circle に対して発生した mousemovediv#square に伝搬してしまうため、マウスが div#square の外に出ても div#circle の表示が続いてしまった。

  • デモ②

div#square の中でマウスを動かすと常にマウスの下に div#circle が表示され、div#circlediv#square での mousemove イベントの発生を妨げてしまうため、マウスが div#circle のうえにいる間はマウスカーソルが表示されてしまった。

つまり両方ともマウスカーソルの下に現れる div#circle がイベントの発生対象であることが原因だとわかります。

そこで、スタイルシートで div#circlepointer-events: none を指定してみます。

これを指定した要素はポインターイベントの対象外となるため、デモ①については mousemove イベントが発生しなくなり、デモ②については div#squaremousemove イベントが発生するようになります。

4. さいごに

イベント伝搬を完全に理解したので次はキャプチャリングを活かしたデザインとか考えてみたいと思います^^

記事は以上になります。最後まで読んでくださりありがとうございます。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?