イベントフロー (event flow)

イベントフローとは

DOM0のイベントモデルでは、イベント発生時にはイベント ターゲット (イベントが発生した要素) のハンドラのみが実行されます。一方でDOM2では、DOM階層における上位の要素のハンドラにも、実行の機会が与えられます

イベントフェーズ (event phase)

イベントは、次の3つの段階 (フェーズ) を経て伝播されます。

  1. キャプチャフェーズ … DefaultView (Window) からドキュメント ツリーを下って、ターゲット ノードまでイベントが伝播する段階です。
  2. ターゲットフェーズ …ターゲット ノードでイベントが呼ばれる段階です。(DOM0のイベントモデルに相当します)
  3. バブルフェーズ … ターゲット ノードからドキュメント ツリーを上って、DefaultViewまでイベントが伝播する段階です。

イベントを捕捉したハンドラはEvent.stopPropagation()で、そのイベントの伝播を停止できます。よってすべてのイベントフェーズが、つねに処理されるとは限りません。

また、イベントによってはバブリング (bubbling) しないものもあります。たとえば、

  • blur
  • focus
  • load
  • unload

が、それです。

イベントフェーズ (event phase)

capture phase
(キャプチャリング)
the event object must propagate through the target's ancestors from the defaultView to the target's parent.

This phase is also known as the capturing phase.

Event listeners registered for this phase must handle the event before it reaches its target.

target phase
(ターゲット)
the event object must arrive at the event object's event target.

This phase is also known as the at-target phase.

Event listeners registered for this phase must handle the event once it has reached its target.

If the event type indicates that the event must not bubble, the event object must halt after completion of this phase.

bubble phase
(バブリング)
the event object propagates through the target's ancestors in reverse order, starting with the target's parent and ending with the defaultView.

This phase is also known as the bubbling phase.

Event listeners registered for this phase must handle the event after it has reached its target.

3.1 Event dispatch and DOM event flow - DOM Level 3 Events Specification

イベントフロー制御の実際

DOMレベル2のイベントは、addEventListener()によって登録できます。

target.addEventListener( type, listener [, useCapture ] );
EventTarget.addEventListener - DOM | MDN

ここからは、このメソッドの第3引数useCaptureをtrueとした場合の挙動について検証していきます。

前提

対象とする要素を、次のように定義します。

<div id="a1">A1
  <div id="a2">A2
    <div id="a3">A3
    </div>
  </div>
</div>

これは、ブラウザでは次のように表示されます。

A1
A2
A3

そしてスクリプトを以下のように記述します。このスクリプトでは各要素にclickイベントを登録し、クリック時にその要素のIDがアラートで出力されるようにしています。

var a1 = document.getElementById( 'a1' );
var a2 = document.getElementById( 'a2' );
var a3 = document.getElementById( 'a3' );

a1.addEventListener( 'click', OnClick, false );
a2.addEventListener( 'click', OnClick, false );
a3.addEventListener( 'click', OnClick, false );

function OnClick( event )
{
    var e = event || window.event;
    alert( e.currentTarget.id );
}

検証

a1、a2、a3のすべての要素のハンドラのuseCaptureをfalse、つまり既定値に設定すると、

  • id="a3"の要素をクリック … a3、a2、a1
  • id="a2"の要素をクリック … a2、a1

の順にアラートが表示されます。つまり、DOM階層の下位から上位に向かってイベントが捕捉されます。次に、a2のハンドラの引数をtrueにしてみます。

a1.addEventListener( 'click', OnClick, false );
a2.addEventListener( 'click', OnClick, true );
a3.addEventListener( 'click', OnClick, false );
  • "a3"をクリック … a2、a3、a1

そうすると、このようにa2の要素が先に捕捉されるようになります。さらにa1のハンドラもtrueにすると、

a1.addEventListener( 'click', OnClick, true );
a2.addEventListener( 'click', OnClick, true );
a3.addEventListener( 'click', OnClick, false );
  • "a3"をクリック … a1a2、a3

のようになり、a1がa2よりも先に捕捉されるようになりました。つまりuseCaptureの値によって、

  • false … DOM階層の下位から上位
  • true … DOM階層の上位から下位

のように捕捉される順番が決定されることがわかります。

イベント伝播の停止

ハンドラがEvent.stopPropagation()を呼び出すと、それ以降イベントは伝播しなくなります。これを検証するため前項のコードを一部修正し、a2のハンドラがstopPropagation()を呼び出すようにしてみます。

a1.addEventListener( 'click', OnClick, false );
a2.addEventListener( 'click', OnClick, false );
a3.addEventListener( 'click', OnClick, false );

function OnClick( event )
{
    var e = event || window.event;
    alert( e.currentTarget.id );

    if( e.currentTarget.id == 'a2' )
    {
        e.stopPropagation();
    }
}

そうすると、

  • id="a3"の要素をクリック … a3、a2
  • id="a2"の要素をクリック … a2
  • id="a1"の要素をクリック … a1

このようになり、a2の要素でイベント伝播が停止しているのがわかります。次に、a2のハンドラのuseCaptureをtrueにしてみます。

a1.addEventListener( 'click', OnClick, false );
a2.addEventListener( 'click', OnClick, true );
a3.addEventListener( 'click', OnClick, false );
  • "a3"をクリック … a2
  • "a2"をクリック … a2
  • "a1"をクリック … a1

前述したように、useCaptureをtrueとすると優先して捕捉されるようになります。このとき伝播を停止するとそれ以降のハンドラでは捕捉されなくなり、結果としてこの例ではa2の要素しか呼ばれなくなっています。

ちなみにa1のハンドラもtrueとすると、

a1.addEventListener( 'click', OnClick, true );
a2.addEventListener( 'click', OnClick, true );
a3.addEventListener( 'click', OnClick, false );
  • "a3"をクリック … a1、a2
  • "a2"をクリック … a1、a2
  • "a1"をクリック … a1

のような結果となります。これはDOM階層の上位が優先して捕捉され、かつa2で伝播が停止されているためです。

JavaScriptのドキュメントから検索