例で始めてみましょう。

このハンドラは <div> へ割り当てられますが、<em><code> のような任意のネストされたタグをクリックしたときにも実行されます:

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

少し奇妙に見えますよね? なぜ実際のクリックが <em> だった場合に <div> 上のハンドラが実行されるでしょう?

バブリング(Bubbling)

バブリングの原理はシンプルです。

要素上でイベントが起きると、最初にその上のハンドラが実行され、次にその親のハンドラが実行され、他の祖先に到達するまでそれらが行われます。

たとえば、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> をクリックすると、3つのアラートが表示されます: pdivform

このプロセスは “バブリング” と呼ばれます。なぜなら、水の中の泡のように、イベントが内部の要素から親に至るまで “バブル” しているためです。

ほぼ すべてのイベントがバブルします

このフレーズのキーワードは、“ほとんど” です。

例えば、focus イベントはバブルしません。他にも例があり、私たちはそれらを見ていくでしょう。しかし、それはルールというよりはむしろ例外であり、ほとんどのイベントはバブルします。

event.target

親要素のハンドラは、常に実際に発生した場所についての詳細を取得できます。

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

this (=event.currentTarget) との違いは次です:

  • event.target – はイベントを始めた “ターゲット” 要素で、バブリングプロセスを通して変化しません。
  • this – は “現在” の要素で、現在実行中のハンドラを持ちます。

例えば、単一のハンドラ form.onclick を持っている場合、その form 中のすべてのクリックを “キャッチ” できます。どこでクリックされたかは関係なく、<form> までバブルし、そのハンドラを実行します。

form.onclick ハンドラの中は:

  • this (=event.currentTarget) は <form> 要素です。なぜなら、ハンドラはそこで動いているからです。
  • event.target は実際にクリックされた form の内側にある具体的な要素です。

見てみましょう:

結果
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>
  クリックすると、<code>event.target</code> と <code>this</code> が表示されます。比較してみましょう。

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

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

event.targetthis が等しい場合があります。つまりクリックを <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つのイベントに対し複数のイベントハンドラを持っている場合、それらの1つがバブリングを止めたとしても、残りのイベントハンドラは引き続き実行されます。

つまり、event.stopPropagation() は上に移動するのは止めますが、現在の要素上にある他のすべてのハンドラは実行します。

バブリングを止め、現在の要素のハンドラを実行しないようにするために、event.stopImmediatePropagation() メソッドがあります。この後は他のハンドラは実行されません。

必要なければバブリングは止めないでください!

バブリングは便利です。本当に必要な場合を除いて止めないでください。: 明白で構造的によく知られているような場合。

event.stopPropagation() は後に問題になるかもしれない隠れた落とし穴を作る場合があります。

例えば:

  1. 私たちはネストされたメニューを作ります。各サブメニューはその要素上でクリックを処理し、外部のメニューがトリガされないよう、stopPropagation を呼び出します。
  2. 後ほど、ユーザの行動(人々がクリックした場所)を追跡するためにウィンドウ全のクリックをキャッチすることに決めました。いくつかの分析システムはそのようなことをします。通常、すべてのクリックをキャッチするためのコードは document.addEventListener('click'…) を使います。
  3. 我々の分析は、クリックが stopPropagation で止められた領域上では動作しません。それは “デッドゾーン” になります。

通常、本当にバブリングを防がなければならないケースはほとんどありません。必要と思われるタスクは他の手段で解決できる可能性があります。そのうちの1つはカスタムイベントを利用することで、後でそれを説明します。また、データをあるハンドラの event オブジェクトに書き込み、別のハンドラでそれを読み込むこともできるので、親のハンドラに下位の処理に関する情報を渡すことができます。

キャプチャリング(Capturing)

“キャプチャリング” と呼ばれるイベント処理の別のフェーズがあります。実際のコードではほとんど使われませんが、役立つときがあります。

標準 DOM Events はイベント伝搬の3つのフェーズを説明しています。:

  1. キャプチャリングフェーズ – イベントが要素へ下りていきます。
  2. ターゲットフェーズ – イベントがターゲット要素に到達しました。
  3. バブリングフェーズ – イベントが要素から上にバブルします。

これは、テーブル中の <td> をクリックしたときの図で、仕様から抜粋したものです:

つまり: <td> をクリックした場合、イベントは最初に祖先のチェーンを通って要素へ下りていき(キャプチャリング)、ターゲットに到達した後、ハンドラを呼び出しながら上に行き(バブル)ます。

以前バブリングについてのみ話しましたが、それはキャプチャリングフェーズはほとんど使われないためです。通常それは私たちには見えません。

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

キャプチャリングフェーズでイベントをキャッチするには、addEventListener の3つ目の引数を true にする必要があります。

最後の引数は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. HTMLBODYFORMDIVP (キャプチャリングフェーズ, 1つ目のリスナーです)そして、:
  2. PDIVFORMBODYHTML (バブリングフェーズ, 2つ目のリスナーです).

P が2回表示されることに注意してください: キャプチャリングの終わりと、バブリングの開始です。

イベントが捕捉されたフェーズの番号を示すプロパティ event.eventPhase があります。 しかし、私たちは通常ハンドラでそれを知っているので、めったに使用されません。

サマリ

イベントハンドラプロセスです:

  • イベントが発生したとき – それが起きた最もネストされた要素は “ターゲット要素” (event.target) としてラベル付けされます。
  • 次にイベントは addEventListener(...., true) で割り当てられたハンドラを呼び出しながらドキュメントルートから event.target へ下りていきます。
  • その後、イベントは on<event> と3つ目の引数がないもしくは falseaddEventListener を使って割り当てられたハンドラを実行しながら event.target からルートまで上がっていきます。

それぞれのハンドラは event オブジェクトのプロパティにアクセスできます:

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

どのハンドラも event.stopPropagation() を呼び出すことでイベントを止めることができますが、それは推奨しません。

キャプチャリングフェーズはめったに使われません、通常バブリングでイベントを処理します。その背後には論理があります。

実世界では、事故が起こると地方自治体がまず反応します。 彼らは起こった場所を最もよく知っています。 その後、必要に応じてより高いレベルの権限が反応します。

イベントハンドラでも同じです。特定の要素に対してハンドラを設定するコードは、その要素と起きることについて最もよく知っています。特定の <td> のハンドラは、まさにその <td> に適合することができ、それについてすべてを知っています。なので、最初に機会を得るべきです。次に、直近の親もまたそのコンテキストについて知っていますが、知っていることは少し少なくなります。そのようにして、一般的な概念を扱い、最後に実行する最上位の要素まで処理をします。

バブリングとキャプチャリングは次のチャプターで学ぶ非常に強力なイベント処理パターンである “イベント委任(event delegation)” の基盤となります。

チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。