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

# 要約

DOMイベントはその名の通りDOMの構造に従って伝搬する。

バブリング/キャプチャリングをイメージできるかがカギ。

**見た目に騙されちゃダメ**

# 1. はじめに

まずはじめに「へんな動きをするデモ」を紹介します。

次の2つは "四角形の中ではマウスカーソルを非表示にして代わりに円を追従させる" ことを実現しようとしたコードです。

* デモ①

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="YzBmJVw" data-user="www-tacos" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/www-tacos/pen/YzBmJVw">
  mouse-cursor</a> by www-tacos (<a href="https://codepen.io/www-tacos">@www-tacos</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

* デモ②

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="qBgeJLb" data-user="www-tacos" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/www-tacos/pen/qBgeJLb">
  mouse-cursor-parallel</a> by www-tacos (<a href="https://codepen.io/www-tacos">@www-tacos</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

いくらか触ってもらえれば気づくと思いますが、

* デモ①はゆっくり動かすと円が四角形の外に出てしまう

* デモ②はマウスカーソルの表示/非表示が交互に切り替わって点滅してるみたいになる

という動きをします。

なぜこうなるのか？そこにはHTMLの構造とイベント伝搬の仕組みが関わってきます。

# 2. DOMツリー

https://ja.javascript.info/dom-nodes

HTMLは要素が入れ子構造をつくるため木構造で表現することができ、実際に木構造に変換したデータを **"DOMツリー"** といったりします。

例えばデモ①のHTMLは`div#square`の中に`div#circle`があるので次のようなDOMツリーと考えられます。

```mermaid
graph TD;
A(body)
B(div#square)
C(div#circle)
A---B;
B---C;
```

一方でデモ②のHTMLは`div#square`と`div#circle`が並んでいるので次のようなDOMツリーと考えられます。

```mermaid
graph TD;
A(body)
B(div#square)
C(div#circle)
A---B;
A---C;
```

このような違いがあると、例えば`div#circle`に `width: 80%` を指定した場合

* デモ①は`div#square`の横幅の80%となる

* デモ②はbodyの横幅の80%となる

という違いが生じます。

またDOMツリーの違いはイベント伝搬にも影響し、今回はこちらが重要になってきます。

# 3. バブリング/キャプチャリング

https://ja.javascript.info/bubbling-and-capturing

イベントはイベントハンドラを設定した要素でのみ発生するものと思っていましたが、実はそうではなく、親要素/自要素/子要素間で伝搬する仕組みになっています。

通常は、ある要素でイベントが発生するとまずその要素のイベントハンドラーが実行され、次にその要素の親要素のイベントハンドラーが実行されます。

このように親要素に伝搬していく流れを **"バブリング"** といいます。（泡が昇っていく様になぞらえて）

一方で、ある要素でイベントが発生したときにその一番遠い親要素のイベントハンドラーから実行していく流れもあり、これを **"キャプチャリング"** といいます。

以下は各デモのDOMツリーに対してキャプチャリングの流れとバブリングの流れを追記した図です。

* デモ①

```mermaid
graph TD;
A(body)
B(div#square)
C(div#circle)
A-.cap.->B
A---B
B-.bub.->A
B-.cap.->C
B---C
C-.bub.->B
```

* デモ②

```mermaid
graph TD;
A(body)
B(div#square)
C(div#circle)
A-.cap.->B
A---B;
B-.bub.->A
A-.cap.->C
A---C;
C-.bub.->A
```

バブリングとキャプチャリングはどちらか片方しか取り得ないわけではなく、すべてのイベントについてキャプチャリングフェーズとバブリングフェーズがあり、どのフェーズでイベントハンドラを実行するかを自分で決めることができます。

```js:キャプチャリングフェーズを利用する例
// addEventListenerの第3引数にキャプチャをONにする設定値を追加する
div.addEventListener("click", (e) => { console.log("clicked") }, { capture: true });
```

例えばデモ①でいうと、`div#square` にキャプチャリングフェーズで動くイベントハンドラＡとバブリングフェーズで動くイベントハンドラＢを設定していて、`div#circle` で目的のイベントが発生した場合、

1. キャプチャリングフェーズを `body` → `div#square` → `div#circle` と進めていき、その途中で `div#square` に設定されたイベントハンドラＡを実行する
2. `div#circle` まで到達してキャプチャリングフェーズ終了（バブリングフェーズに移行）
3. バブリングフェーズを `div#circle` → `div#square` → `body` と進めていき、途中で `div#square` に設定されたイベントハンドラＢを実行する

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

また、イベントの伝搬は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` を指定してみます。

https://developer.mozilla.org/ja/docs/Web/CSS/pointer-events#none

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

# 4. さいごに

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

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