logo logo

July 02, 2019 10:49

猿でも理解できるJavaScriptイベントと制御

概要

この記事ではJavaScript(及びそのライブラリであるjQueryやフレームワークを含む)で多用するイベントについて知っておくべきイベントフローという概念とその仕様について、猿と木とバナナを例に出しながら理解していきます。このイベントフローが理解できると、ご利益として

  • 親子関係のDOMにイベントを仕込んだときに毎度意図しない挙動をする
  • 毎回stopPropagationとかpreventDefaultとかreturn false;を使ってるねじ伏せてるけど、正直よくわからないで使ってる
  • useCaptureとかあるけど、上のやつとどう関係してんの?

みたいな悩みがすっきりします。最低限の理解すべき仕様とポイントを例を交えながら明らかにします。

イベントフローとは?

イベントフローは何かDOMに関連するイベントが発生したときに、各ブラウザがそれらを処理する一連の手続きのことです。W3Cの上の図がわかりやすいですね。これらは次の3つのフェイズに分解されます。

  • キャプチャーフェイズ:DOMを最下層のrootから順繰りに辿り、イベント発火した場所をつきとめていく
  • ターゲットフェイズ:イベント発火の場所に到達するフェイズ
  • バブリングフェイズ:今度は元来た経路を引き返しながら、子から親へ逆戻りしていく

DOMツリーを文字通り、JavaScriptの処理系を、イベントリスナーをバナナ、イベントそのものは木の特定の箇所でおこる何らかのイベント(ガサガサ)と表現することにします。

この猿は木でガサガサと音がすると、そこに向かって一直線に木を登っていきます。この往路がキャプチャーフェイズです。デフォルトではその途中にバナナがあっても、猿は目もくれず、まずはガサガサの起こった場所に到達します。これがターゲットフェイズです。ターゲットフェイズにバナナがあればバナナを取得、もといイベントリスナーに伴う処理が発動します。その後猿は元来た経路をたどりながら地上に帰りますが、その過程でバナナがあれば取得しつつ、元に戻るといったフローになります。

イベントリスナー発動の制御

さて、この仕様通りだと、実際にはプログラマーの意図に反することが起こりえます。
例えば子DOMと親DOMにクリックイベントリスナーを仕掛けたとき、子DOMをクリックされたら、子DOMのイベントだけ発火してほしいのに、子→親の順でイベントが発生するといったパターンです。猿にはガサガサが起こったところのバナナだけとってくれればいいのに、そうはならないで全部持ち帰ってしまう。場合によったら先に手前のバナナを取ってから奥のバナナを拾ってほしいみたいなこともあるかもしれません。aリンクをクリックしたときに状況によってはジャンプさせないでイベントだけ発動したい。つまり仕掛けたつもりのないバナナを猿が拾ってしまう場合などがあります。

そんなときにイベントフローを制御して、プログラマーの意図通りにイベントを発火するために、次のようなオプションとその組み合わせが考えられます。

  • e.stopPropagation()
  • e.preventDefault()
  • return false;
  • useCaptureオプション

順番に見ていきましょう。

event.stopPropagation()

キャプチャー→ターゲット以降のバブリングフェイズで、イベントが伝播するのを止める処理になります。先ほどの例でいうと、子と親にクリックイベントがあるんだけど、子DOMがクリックされたときは、それに付随するイベントだけを発火させたいときに使えますね。
言い換えると猿がガサガサに到達してバナナをとってきたら、道草食わないで飛び降りて戻ってこいという命令になります。

event.preventDefault()

これは文字通りデフォルトで仕掛けられているイベントリスナーの発動を止める処理になります。
例えばaリンクタグのクリックで、別のイベントを発火させて飛ばしたくないといったときに使います。DOMツリーには元からなるバナナが隠れているわけですが、ガサガサが起きたとき、猿にそれはひろって欲しくない場合があるかもしれません。そんなときに使える命令です。

return false;

これはとにもかくにも、イベントフロー中に猿を強制的に木から引きづりおろす命令です。
あまり難しく考える必要はないですね。

useCaptureオプション

これはaddEventListenerに与えられるオプション引数で、デフォルトではfalseになっています。
addEventListener()

document.getElementById('#target').addEventListener('click', function(e){ console.log('target clicked'); }, true);

とかって書いたときの三番目のオプション引数ですね。
これをtrueにすると、猿がガサガサを目指している途中にバナナを見つけた場合はそれを先にとって来いという指定を与えることになります。親子それぞれクリックリスナーが仕込まれていて、子がクリックしたときに、親→子という順で発動させたい場合にはtrueにすると達成できます。

尚、jQueryではdefaultのfalseが固定となっており、このオプションを使えないことに注意が必要です。

まとめ

  • DOM上でクリックやマウス移動などのイベント(ガサガサ)が発火するたびに、お猿さんはそこに行って帰ってくるを繰り返している。
  • 猿回しであるプログラマーのあなたは、仕掛けたバナナを取ってくる順番、特定のバナナだけを取ってきたいなどの目的に応じて相応の命令をお猿に与える必要がある
    • event.stopPropagation() : ガサガサに到達したら道草食わずに帰ってこい
    • event.preventDefault() : わしが仕掛けたバナナだけとってこい。野生のバナナはとるな
    • return false; : 帰ってこい!今すぐ!
    • useCapture = true : 往路でバナナあったらとってこい