ちゃんと知ってる?JavaScriptのイベントバブリングを学ぼう

2017/07/05

Giulio Mainardi

116

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

イベントが発生したとき、親やさらに先の要素にも同じイベントが発生する「イベントバブリング」。バグをなくし、思い通りに制御できるようになるために、しっかり理解しましょう。

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つのフェーズに分かれます。

  1. ウィンドウからイベントターゲットの親まで:キャプチャーフェーズ
  2. イベントターゲット:ターゲットフェーズ
  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'));

ログテーブルを確認するとc1c2ウィンドウのcapturer行にもなにも出力されていません。新しいリスナーの実行後、伝搬が止まったことがわかります。

イベントのキャンセル

伝搬の最後にブラウザーが実行するデフォルト動作に関連するイベントがあります。たとえば、リンク要素をクリックしたり、送信ボタンをクリックすると、それぞれ、ブラウザーが新しいページに移動したり、フォームを送信したりします。

イベントをキャンセルすれば、デフォルト動作の実行が止まります。リスナーで、e.preventDefaultイベントオブジェクトの別のメソッドを呼びます。

参考文献

本記事はYaphi BerhanuDominic Myersが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:What Is Event Bubbling in JavaScript? Event Propagation Explained

[翻訳:関 宏也/編集:Livit

Copyright © 2017, Giulio Mainardi All Rights Reserved.

Giulio Mainardi

Giulio Mainardi

フロントエンド開発について少し勉強中です。

Loading...