JavaScriptを使っている人なら、「イベントバブリング」を聞いたことがあると思います。ある要素が別の要素にネストされて、両方の要素がクリックなど同じ「イベント」のリスナーを登録しているときにイベントハンドラーが呼ばれる順番のコンセプトです。
イベントバブリングはパズルの1片にすぎず「イベントキャプチャリング」や「イベント伝搬(プロパゲーション)」と一緒に話題になります。JavaScriptでイベントを扱うには、この3つを理解しなければなりません。たとえば、the event delegation patternを身に付けたいときです。
この記事では、用語を説明し、それらがどう関連するかを解説します。JavaScriptのイベントフローの基本を理解すれば、アプリケーションをきめ細かく制御できるようになります。
ただし、イベントの入門記事ではありません。イベントの知識があることを前提とします。
イベント伝搬とは
イベント伝搬は、イベントバブリングとイベントキャプチャリングの両方をカバーする用語です。サムネイルギャラリーにリンクされたイメージをリストにするマークアップで解説します。
<ul>
<li><a href="..."><img src="..." alt=""></a>
<li><a href="..."><img src="..." alt=""></a>
...
<li><a href="..."><img src="..." alt=""></a>
</ul>
画像をクリックすると、対応するIMG要素のclickイベントが生成され、windowオブジェクトを終了するまで、親A、祖父LIと、要素の先祖をすべてさかのぼってイベントが生成されます。
DOM用語では、画像はイベントターゲットであり、一番内側の要素でクリックが発生したことになります。イベントターゲットを親からたどってwindowオブジェクトまでが、DOMツリーのブランチの構成です。イメージギャラリーのブランチの構成はIMG、A、LI、UL、BODY、HTML、document、windowで構成されます。
windowは、DOMノードではありませんが、EventTargetインターフェイスのインプリメントを簡単にするために、documentオブジェクトの親ノードのように扱います。
このブランチは、イベントが伝搬する(流れる)経路です。伝搬は、ブランチ上のノードに付与された既定のイベントタイプのリスナーをすべて呼び出していくプロセスです。各リスナーはeventオブジェクトと一緒に呼び出され、イベントに関連する情報を集めます(これについては後述)。
同じイベントタイプに対して、1つのノードに複数のリスナーを登録できます。伝搬がノードに達すると、登録された順にリスナーが呼び出されます。
また、ブランチは静的に決定します。イベントが最初に実行されたときに確立されるのです。イベント処理中に起こったツリーの変更は無視します。
伝搬はウィンドウからイベントターゲットの方向とその逆の双方向です。伝搬は3つのフェーズに分かれます。
- ウィンドウからイベントターゲットの親まで:キャプチャーフェーズ
- イベントターゲット:ターゲットフェーズ
- イベントターゲットの親から戻ってウィンドウまで:バブルフェーズ
呼ばれるリスナーのタイプによりフェーズが異なります。
イベントキャプチャーフェーズ
このフェーズでは、addEventListenerの3番目のパラメータをtrueとして登録されたcapturerリスナーが呼ばれます。
el.addEventListener('click', listener, true)
このパラメータを省略すると、デフォルト値のfalseなので、リスナーはcapturerではありません。
したがって、このフェーズでは、ウィンドウからイベントターゲットの親までのパスで見つかったcapturerが呼ばれます。
イベントターゲットフェーズ
このフェーズでは、イベントターゲットに登録されたすべてのリスナーが、captureフラグの値にかかわらず呼び出されます。
イベントバブリングフェーズ
イベントバブリングフェーズで呼ばれるのは、capturer以外のリスナーになります。具体的にはaddEventListener()の3番目のパラメータがfalseで登録されているリスナーです。
el.addEventListener('click', listener, false) // listener doesn't capture
el.addEventListener('click', listener) // listener doesn't capture
キャプチャーフェーズでは、イベントターゲットまで、focus、blur、loadなど、すべてのイベントが下向きに流れます。伝搬はターゲットフェーズで終了します。
したがって、伝搬の最後では、ブランチの各リスナーは1度だけ呼ばれます。
イベントバブリングは、すべての種類のイベントで起こるわけではありません。伝搬中、リスナーはeventオブジェクトの.bubblesブーリアンプロパティを読めば、イベントがバブルしているか分かります。
W3C UIEvents specificationから引用した3つのイベントフローフェーズを説明した図です。
伝搬情報を手に入れる
eventオブジェクトの.bubblesプロパティは、伝搬の情報にリスナーがアクセスするために使えるものがほかにもあります。
- e.target:イベントターゲットを参照する
- e.currentTarget:実行中のリスナーが登録されているノード。リスナーの呼び出しコンテクスト、つまり、thisキーワードで参照される値と同じ値
- e.eventPhase:現在のフェーズが分かる。3つのEventコンストラクタ定数CAPTURING_PHASE、BUBBLING_PHASE、AT_TARGETの1つを参照する整数
実際に使ってみる
説明してきた概念を実際に使います。次のコードペンは、入れ子になった5つの箱があり、b0・・・b4の名前がついています。外側の箱b0だけが見えます。内側の箱はマウスポインターが上に来たときに現れます。どれか箱をクリックすると、伝搬フローのログが右のテーブルに表示されます。
箱の外もクリックできます。イベントターゲットは、クリックスクリーンの場所に従って、BODYまたはHTML要素となります。
伝搬を止める
イベントオブジェクトのstopPropagationメソッドを呼べば、どのリスナーからでもイベント伝搬を止められます。現在のターゲットに続く伝搬パス上のノードで登録されているリスナーはすべて呼ばれなくなります。そのかわり、現在のターゲットに附随する残りすべてのリスナーは、イベントを受け取り続けます。
この動作は、simple fork of the previous demoで確かめられます。リスナーの1つにstopPropagation()を呼ぶ命令を挿入するだけです。windowに登録されたコールバックのリストの先頭に、capturerとして新しいリスナーを追加します。
window.addEventListener('click', e => { e.stopPropagation(); }, true);
window.addEventListener('click', listener('c1'), true);
window.addEventListener('click', listener('c2'), true);
window.addEventListener('click', listener('b1'));
window.addEventListener('click', listener('b2'));
どの箱がクリックされても、伝搬は止まり、ウィンドウのcapturerリスナーだけに届きます。
ただちに伝搬を止める
名前のとおり、stopImmediatePropagationはただちにブレーキをかけ、現在のリスナーの子がイベントを受け取るのを禁止します。minimal change to the last penで確かめられます:
window.addEventListener('click', e => { e.stopImmediatePropagation(); }, true);
window.addEventListener('click', listener('c1'), true);
window.addEventListener('click', listener('c2'), true);
window.addEventListener('click', listener('b1'));
window.addEventListener('click', listener('b2'));
ログテーブルを確認するとc1とc2ウィンドウのcapturer行にもなにも出力されていません。新しいリスナーの実行後、伝搬が止まったことがわかります。
イベントのキャンセル
伝搬の最後にブラウザーが実行するデフォルト動作に関連するイベントがあります。たとえば、リンク要素をクリックしたり、送信ボタンをクリックすると、それぞれ、ブラウザーが新しいページに移動したり、フォームを送信したりします。
イベントをキャンセルすれば、デフォルト動作の実行が止まります。リスナーで、e.preventDefaultイベントオブジェクトの別のメソッドを呼びます。
参考文献
- ドキュメントオブジェクトモデル (DOM)レベル2イベント仕様
- W3C DOM4 – イベント
- DOM – 生きた標準 – イベント
- W3C UI イベント – DOMイベントアーキテクチャ
- MSN – イベントとDOM
本記事はYaphi Berhanu、Dominic Myersが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
(原文:What Is Event Bubbling in JavaScript? Event Propagation Explained)
[翻訳:関 宏也/編集:Livit]