JavaScriptのイベントの伝播と制御

Webアプリケーションのページを表示したディスプレイ

ブラウザーで表示されたWebのページ上では

  • ボタンがクリックされる
  • テキストボックスの内容(値)が変更される
  • 要素の上にマウスポインターが乗ったり外れたりする
  • キーボードの特定のキーが押される。
  • 画面の情報が送信(サブミット)される

など、ユーザーの様々な操作によって発生する出来事が発生します。
JavaScriptではこの出来事をイベントという単位で扱い、イベントに応じて実行するコード(処理)を記述してページに動きを付けます。
ユーザーの操作で発生するイベントをきっかけに処理を記述するプログラミングモデルは「イベント駆動型モデル(イベントドリブンモデル)」と呼ばれています。
イベントをもとに処理を行うプログラミング言語はJavaScript以外にもたくさんありますが、JavaScriptのイベントが他のプログラミング言語と違う点は、HTMLのDOM階層でイベントが伝播するという点です。

そこで今回は、JavaScriptのイベントの伝播と、イベントの制御について紹介します。

イベントの伝播

イベントはボタンやテキストボックスなど、ユーザーが操作する要素(ターゲット)のみで発生するのではなく、HTMLのDOM階層をたどって親階層にある要素や子階層にある要素でも発生します。

イベント伝播のイメージ

イベントの伝播を図にすると以下のようになります。

JavaScriptイベントの伝播イメージ

上記の図では、イベントが発生するのはform要素内に配置されたbutton要素ですが、親要素のform要素やdiv要素でもイベントが発生します。

イベント伝播を確認するサンプルコード

イベントの伝播を確認するために、以下のコードを記述して実行してみます。

上記の例では、body要素内にdiv要素を配置し、div要素内にform要素を配置し、form要素内にbutton要素を配置しています。
配置したdiv、form、buttonのそれぞれ要素のclickイベントにaddEventListenerメソッドでコンソールにログを出力する処理を割り当てています。
上記のコードを実行すると、コンソールには以下のようにログが出力されます。

JavaScriptイベントの伝播を確認するためのコードを実行した結果のコンソールログ

コンソールログには、まずイベントの発生もとであるbutton要素のclickイベントが発生して「b」が出力され、続いてbutton要素の親要素であるform要素のclickイベントが発生して「f」が出力され、さらにform要素の親要素であるdiv要素のclickイベントが発生して「d」が出力されます。

上記の結果からサンプルのコードでは、ターゲットフェーズでイベントが発生し、その後バブリングフェーズで下位の要素から上位の要素にイベントが伝播して発生していることが確認できます。

イベント各フェーズ

イベントのフェーズには、「キャプチャフェーズ」「バブリングフェーズ」「ターゲットフェーズ」の3つがあります。

キャプチャフェーズでは、Webページ(HTMLのDOM階層)の最上位のwindowオブジェクトから文書ツリーをたどって、下位の要素(子階層の要素)にイベントが伝播します。
そして、ターゲットフェーズでイベントの発生元の要素を特定します。

バブリングフェーズは、イベントの発生元の要素から上位の要素(親階層の要素)に向かってイベントが伝播していきます。
最終的には、最上位のwindowオブジェクトに到達したところでイベントの伝播が終了します。

キャプチャフェーズ(キャプチャリングフェーズ)

HTMLページ内でクリックなどのユーザーの操作が発生すると、一番最上位のオブジェクトであるwindowオブジェクトでイベントが発生します。
そして、documentオブジェクト、html要素、body要素、div要素とDOMツリーを買いの階層は向かって順番にたどりながらイベントが発生していきます。

「イベント伝播を確認するサンプルコード」の例では、windowオブジェクトのclickイベントからスタートして、document → html → body → div → form → buttonの順にclickイベントが発生します。

JavaScriptの既定の動作では、キャプチャフェーズで発生したイベントは、使用されることが少ないため、イベントハンドラを登録してもイベントハンドラ(メソッド)は呼び出されません。

キャプチャフェーズでイベントを発生させるには、addEventListenerメソッドでイベントリスナーを登録するときに、第3引数のuseCaptureにtrueを指定します。
先に示したbutton要素のイベントをaddEventListenerメソッドでリスナー登録するコードであれば、以下のようになります。

第3引数の指定は「true」以外にも「{capture: true}」と指定することもできます。

addEventListenerメソッドの第3引数は、デフォルト(既定値)ではfalseになっていますので、trueを指定しない(省略した)場合は、キャプチャフェーズで発生したイベントでイベントリスナーが呼び出されません。

EventTarget.addEventListener() – MDN

ターゲットフェーズ

ターゲットフェーズは、イベントが実際に発生した要素でキャプチャフェーズの後にイベントが発生するフェーズになります。

バブリングフェーズ

キャプチャフェーズ → ターゲットフェーズとフェーズが移行した後に、キャプチャフェーズとは逆にイベントが発生した要素の親要素(上記の例ではform要素)からwindowオブジェクトまでDOMツリーを上位階層へたどって、順番にイベントが発生していくフェーズがバブリングフェーズになります。
バブリングフェーズでは、イベントが発生した要素で発生したイベントと同じイベントが親階層をたどって発生していきます。(上記の例ではclickイベント)

バブリングフェーズで発生したイベントでは、イベントに登録されたイベントリスナー(メソッド)が呼び出されます。
ただし、focusイベントやblurイベントなどの一部のイベントではバブリングフェーズでイベントリスナーが呼び出されません。
これは、イベントのEvent.bubblesプロパティがfalseとなっているためです。

キャプチャフェーズでのイベント発生サンプル

最後に、「イベント伝播を確認するサンプルコード」で先に示したコードのaddEventListenerの第3引数にtrueを指定し、キャプチャフェーズでイベントリスナーが呼び出されるようにした場合の例を示します。

上記のコードを実行すると、イベントの発生順が逆になることが確認できます。

JavaScriptイベントの伝播を確認するためのコードを実行した結果のコンソールログ(キャプチャフェーズで実行)