2022年10月14日

バブリングとキャプチャリング

例から始めましょう。

このハンドラは`

`に割り当てられていますが、``や``のようなネストされたタグをクリックした場合にも実行されます。

<div onclick="alert('The handler!')">
  <em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em>
</div>

少し奇妙ではありませんか?`<div>`上のハンドラが、実際のクリックが`<em>`上で行われた場合に実行されるのはなぜでしょうか?

バブリング

バブリングの原理は簡単です。

要素でイベントが発生すると、最初にその要素のハンドラが実行され、次に親要素、そして他の祖先要素へと順番に実行されます。

例えば、各要素にハンドラを持つ3つのネストされた要素`FORM > DIV > P`があるとします。

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form onclick="alert('form')">FORM
  <div onclick="alert('div')">DIV
    <p onclick="alert('p')">P</p>
  </div>
</form>

内部の`<p>`をクリックすると、最初に`onclick`が実行されます。

  1. その`<p>`上で。
  2. 次に外部の`<div>`上で。
  3. 次に外部の`<form>`上で。
  4. そして、`document`オブジェクトまで上向きに伝わります。

そのため、`<p>`をクリックすると、`p`→`div`→`form`の3つのアラートが表示されます。

このプロセスは「バブリング」と呼ばれ、イベントは水中の泡のように内部要素から親要素へと「バブル」のように伝播します。

ほとんどすべてのイベントはバブリングします。

このフレーズのキーワードは「ほとんど」です。

例えば、`focus`イベントはバブリングしません。他にも例がありますが、それは規則というよりは例外であり、ほとんどのイベントはバブリングします。

event.target

親要素上のハンドラは、常にそれが実際にどこで発生したかについての詳細を取得できます。

イベントを引き起こした最も深くネストされた要素は、ターゲット要素と呼ばれ、`event.target`としてアクセスできます。

`this`(= `event.currentTarget`)との違いに注意してください。

  • `event.target` – イベントを開始した「ターゲット」要素であり、バブリングプロセス全体で変化しません。
  • `this` – 「現在の」要素であり、現在その上でハンドラが実行されている要素です。

例えば、単一のハンドラ`form.onclick`がある場合、それはフォーム内のすべてのクリックを「キャッチ」できます。クリックがどこで発生しても、`<form>`までバブリングしてハンドラを実行します。

`form.onclick`ハンドラ内では

  • `this`(= `event.currentTarget`)は`<form>`要素です。なぜなら、ハンドラはそこで実行されるからです。
  • `event.target`は、クリックされたフォーム内の実際の要素です。

確認してみましょう

結果
script.js
example.css
index.html
form.onclick = function(event) {
  event.target.style.backgroundColor = 'yellow';

  // chrome needs some time to paint yellow
  setTimeout(() => {
    alert("target = " + event.target.tagName + ", this=" + this.tagName);
    event.target.style.backgroundColor = ''
  }, 0);
};
form {
  background-color: green;
  position: relative;
  width: 150px;
  height: 150px;
  text-align: center;
  cursor: pointer;
}

div {
  background-color: blue;
  position: absolute;
  top: 25px;
  left: 25px;
  width: 100px;
  height: 100px;
}

p {
  background-color: red;
  position: absolute;
  top: 25px;
  left: 25px;
  width: 50px;
  height: 50px;
  line-height: 50px;
  margin: 0;
}

body {
  line-height: 25px;
  font-size: 16px;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="example.css">
</head>

<body>
  A click shows both <code>event.target</code> and <code>this</code> to compare:

  <form id="form">FORM
    <div>DIV
      <p>P</p>
    </div>
  </form>

  <script src="script.js"></script>
</body>
</html>

`event.target`が`this`と等しくなる可能性があります。これは、`<form>`要素を直接クリックした場合に発生します。

バブリングの停止

バブリングイベントは、ターゲット要素から直接上向きに伝わります。通常は`<html>`まで、そして`document`オブジェクトまで、さらには一部のイベントは`window`まで到達し、そのパス上のすべてのハンドラを呼び出します。

しかし、任意のハンドラは、イベントが完全に処理されたと判断して、バブリングを停止することができます。

そのためには、`event.stopPropagation()`メソッドを使用します。

例えば、ここで`<button>`をクリックすると`body.onclick`は機能しません。

<body onclick="alert(`the bubbling doesn't reach here`)">
  <button onclick="event.stopPropagation()">Click me</button>
</body>
event.stopImmediatePropagation()

要素が単一のイベントに複数のイベントハンドラを持つ場合、1つのハンドラがバブリングを停止しても、他のハンドラは実行されます。

言い換えれば、`event.stopPropagation()`は上向きの移動を停止しますが、現在の要素では他のすべてのハンドラが実行されます。

バブリングを停止し、現在の要素上のハンドラの処理を停止するには、`event.stopImmediatePropagation()`メソッドを使用します。これ以降、他のハンドラは実行されません。

必要のないバブリングを停止しないでください!

バブリングは便利です。本当に必要がない限り、停止しないでください。明白で、設計上よく考えられています。

場合によっては、`event.stopPropagation()`が隠れた落とし穴を作り出し、後で問題になる可能性があります。

例えば

  1. ネストされたメニューを作成します。各サブメニューは、その要素のクリックを処理し、外部メニューがトリガーされないように`stopPropagation`を呼び出します。
  2. 後で、ユーザーの行動(人がどこをクリックするか)を追跡するために、ウィンドウ全体でのクリックをキャッチすることにします。一部の分析システムではそのようなことを行います。通常、コードは`document.addEventListener('click'…)`を使用してすべてのクリックをキャッチします。
  3. `stopPropagation`によってクリックが停止されている領域では、分析機能は機能しません。「デッドゾーン」が発生します。

通常、バブリングを防止する本当の必要はありません。一見そう見えるタスクは、他の手段で解決できる場合があります。その1つはカスタムイベントの使用です(後で説明します)。また、ハンドラでデータを`event`オブジェクトに書き込み、別のハンドラで読み取ることで、下位処理に関する情報を親のハンドラに渡すことができます。

キャプチャリング

「キャプチャリング」と呼ばれるイベント処理のもう1つのフェーズがあります。実際のコードではめったに使用されませんが、場合によっては役立つことがあります。

標準のDOMイベントは、イベント伝播の3つのフェーズについて説明しています。

  1. キャプチャリングフェーズ – イベントは要素に伝わります。
  2. ターゲットフェーズ – イベントがターゲット要素に到達しました。
  3. バブリングフェーズ – イベントは要素からバブルアップします。

これは、仕様から取得した図で、テーブル内の`<td>`に対するクリックイベントのキャプチャリング`(1)`、ターゲット`(2)`、バブリング`(3)`のフェーズを示しています。

つまり、`<td>`をクリックすると、イベントは最初に祖先チェーンを介して要素に伝わります(キャプチャリングフェーズ)。次にターゲットに到達してそこでトリガーされ(ターゲットフェーズ)、次に上向きに伝わります(バブリングフェーズ)。

これまでのところ、バブリングについてのみ説明してきました。なぜなら、キャプチャリングフェーズはめったに使用されないからです。

実際、`on<event>`プロパティまたはHTML属性を使用するか、2引数の`addEventListener(event, handler)`を使用することで追加されたハンドラは、キャプチャリングについて何も知りません。それらは2番目と3番目のフェーズでのみ実行されます。

キャプチャリングフェーズでイベントをキャッチするには、ハンドラの`capture`オプションを`true`に設定する必要があります。

elem.addEventListener(..., {capture: true})

// or, just "true" is an alias to {capture: true}
elem.addEventListener(..., true)

`capture`オプションには2つの可能な値があります。

  • `false`(デフォルト)の場合、ハンドラはバブリングフェーズに設定されます。
  • `true`の場合、ハンドラはキャプチャリングフェーズに設定されます。

正式には3つのフェーズがありますが、2番目のフェーズ(「ターゲットフェーズ」:イベントが要素に到達した)は個別に処理されません。キャプチャリングとバブリングの両方のフェーズのハンドラはそのフェーズでトリガーされます。

キャプチャリングとバブリングの両方を動作させてみましょう。

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>

<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
  }
</script>

このコードは、ドキュメント内のすべての要素にクリックハンドラを設定して、どのハンドラが機能しているかを確認します。

`<p>`をクリックすると、シーケンスは次のようになります。

  1. `HTML`→`BODY`→`FORM`→`DIV`→`P`(キャプチャリングフェーズ、最初のリスナー)
  2. `P`→`DIV`→`FORM`→`BODY`→`HTML`(バブリングフェーズ、2番目のリスナー)。

`P`は2回表示されます。これは、キャプチャリングリスナーとバブリングリスナーの2つのリスナーを設定しているためです。ターゲットは、最初のフェーズの最後と2番目のフェーズの最初にトリガーされます。

`event.eventPhase`というプロパティは、イベントがキャッチされたフェーズの番号を示しています。しかし、ハンドラでは通常それがわかっているので、めったに使用されません。

ハンドラを削除するには、`removeEventListener`は同じフェーズを必要とします。

`addEventListener(..., true)`を使用した場合、ハンドラを正しく削除するには、`removeEventListener(..., true)`で同じフェーズを指定する必要があります。

同じ要素とフェーズのリスナーは、設定された順序で実行されます。

`addEventListener`を使用して同じ要素に同じフェーズに複数のイベントハンドラがある場合、それらは作成されたのと同じ順序で実行されます。

elem.addEventListener("click", e => alert(1)); // guaranteed to trigger first
elem.addEventListener("click", e => alert(2));
キャプチャリング中の`event.stopPropagation()`もバブリングを防ぎます。

`event.stopPropagation()`メソッドとその兄弟である`event.stopImmediatePropagation()`は、キャプチャリングフェーズでも呼び出すことができます。その場合、それ以降のキャプチャリングだけでなく、バブリングも停止します。

言い換えれば、通常イベントは最初に下向きに(「キャプチャリング」)そして上向きに(「バブリング」)伝わります。しかし、キャプチャリングフェーズで`event.stopPropagation()`が呼び出されると、イベントの伝播が停止し、バブリングは発生しません。

まとめ

イベントが発生すると、イベントが発生した最もネストされた要素が「ターゲット要素」(`event.target`)としてラベル付けされます。

  • 次に、イベントはドキュメントルートから`event.target`に伝わって、その途中で`addEventListener(..., true)`を使用して割り当てられたハンドラを呼び出します(`true`は`{capture: true}`の略記です)。
  • 次に、ターゲット要素自体でハンドラが呼び出されます。
  • 次に、イベントは`event.target`からルートまでバブルアップし、`on<event>`、HTML属性、および3番目の引数なしまたは3番目の引数`false/{capture:false}`を使用して`addEventListener`で割り当てられたハンドラを呼び出します。

各ハンドラは、`event`オブジェクトのプロパティにアクセスできます。

  • `event.target` – イベントが発生した最も深い要素。
  • `event.currentTarget`(= `this`)– イベントを処理する現在の要素(ハンドラを持つ要素)。
  • `event.eventPhase` – 現在のフェーズ(キャプチャリング=1、ターゲット=2、バブリング=3)。

イベントハンドラはevent.stopPropagation()を呼び出すことでイベントを停止できますが、それは推奨されません。なぜなら、上流で(おそらく全く異なる目的で)必要になる可能性があるかどうかを確実に判断できないからです。

キャプチャリングフェーズは非常に稀に使用され、通常はバブリングでイベントを処理します。それには論理的な説明があります。

現実世界では、事故が発生した場合、最初に対応するのは地元当局です。彼らは事故現場の状況を最もよく知っています。必要に応じて、その後、上位の当局が対応します。

イベントハンドラも同じです。特定の要素にハンドラを設定したコードは、その要素とその動作に関する最大限の詳細を知っています。特定の<td>要素に対するハンドラは、まさにその<td>要素に適しており、その要素に関するすべてを知っているので、最初に処理する機会を得るべきです。次に、その直上の親要素もコンテキストについてある程度知っており、最上位の要素(一般的な概念を処理し、最後に処理を実行する要素)まで続きます。

バブリングとキャプチャリングは、「イベントデリゲーション」の基礎を築きます。これは、次の章で学ぶ非常に強力なイベント処理パターンです。

チュートリアルマップ

コメント

コメントする前にこれをお読みください…
  • 改善点に関するご提案がある場合は、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 短いコードを挿入するには<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepenなど)を使用してください。