JavaScriptのイベント
以前JavaScriptでエンターキーでフォーカス移動を実装した際、ふとonclickとaddEventListener('click'...の違いは何だろう、という疑問が沸いて出たので、 MDN:「Events」等のページを参考に解決しました。
onXXXとaddEventListenerの違い
多くのコードではその違いを意識する必要はありません。ただ、MDNドキュメントでは次の理由によりaddEventListenerの使用が推奨されていました。
- ひとつのイベントに対して複数のハンドラーを設定することができる
- リスナーが起動されるときのフェーズを細かく制御できる
- あらゆるイベントターゲットで動作する
言葉の意味は後で整理しますが、これらが二者の違いです。
これに、筆者が補足するとすれば次の点です。
on...はオブジェクトに組み込まれたイベント用のプロパティで、ここに設定したコードは、対象となるイベントが発生した際に実行されます。プロパティなのでelement.onclick()として、イベントとは無関係に関数として呼び出すことも可能です。
一方の、addEventListenerはメソッドで、オブジェクトにリスナーを組み込んでいます。
onXXXの方の利点を上げるとすれば、インライン(HTML中の属性にon...と書く)ことができることですが、これはコードが見にくくなる原因なのでやめた方がいいとMDNは言っています。HTML文書とJavaScriptはきちんと区分けするのが後の管理が楽です。
JavaScriptイベント関連の用語
ここでイベントに関連する言葉の意味をまとめておきます。
- イベント
発生する動作や、出来事です。click(クリック)やload、errorなどがあります。
- ターゲット
event.targetから取得できるものからも想像できると思いますが、イベントの発生源となったものです。
- ハンドラー
イベントの発生時に実行される一連のコードを指します。
onClickなど、onXXXや、addEventListenerの第2引数に関数を書くと思いますが、それらのことを指します。
- リスナー
ハンドラーと同じ意味で用いられることも多いですが、正確にはリスナーはイベントの発生を監視するもの、ハンドラーはイベントにたいして動作するコードという違いがあります。
- 伝播
伝搬、プロパゲーション(propagation)とも言います。
イベントが発生源からHTML要素に向かって、またはHTMLから発生源に向かって伝わっていくことです。(筆者はWindowから発生源に向かう流れのことも指すとは知りませんでした)
発生源からHTMLに向かう流れをバブリング、HTMLから発生源に向かう流れをキャプチャリングと呼びます。
バブリング・キャプチャリング
バブリング・キャプチャリングの2種の流れがある理由は、そのような挙動の違う二種のブラウザがはじめに存在しそれを統合した歴史的な理由からなのだそうです。
addEventListenerでは第3引数の値をtrueにすることでキャプチャリングを設定することができます。省略時はバブリングとなっています。onXXXではバブリングだけです。
addEventListenerでキャプチャリングとバブリングを混在させると、イベントの伝播は、
- キャプチャリング対象のイベント
- ターゲットオブジェクトのイベント
- バブリング対象のイベント
となります。ターゲットオブジェクトのイベントは第3引数の値に関係なくキャプチャリングとバブリングの間になります。
次のようなコードを書くと、いつもと違う挙動になります。
sample.html
<div id="outer" style="margin: 5px; background:#ccc">
outer(キャプチャリング)
<div id="inner" style="margin: 5px; background:#ccf">
inner(バブリング)
<div id="target" style="margin: 5px; background:#cfc">
target(キャプチャリング)
</div>
</div>
</div>
<script>
document.getElementById("outer").addEventListener("click",() => { alert("outer");}, true);
document.getElementById("inner").addEventListener("click",() => { alert("innerr");}, false);
document.getElementById("target").addEventListener("click",() => { alert("target");}, true);
</script>
バブリングとキャプチャリングの挙動のサンプルです。targetの部分をクリックすると、outer→target→innerの順でイベントが発生します。
それぞれの過程ことをフェーズといい、フェーズはキャプチャリングフェーズ、ターゲットフェーズ、バブリングフェーズと遷移していきます。
バブリングとキャプチャリングとは直接関係ありませんが、addEventListenerで同じオブジェクト、同じイベントで複数のイベントが設定されていた場合の順序は登録順となります。
addEventListenerの第3引数
先ほどの例では、addEventListenerの第3引数にキャプチャリング対象か否かのboolean値を渡しましたが、比較的新しいブラウザではオブジェクトを渡す文法もあります。
第3引数のオブジェクトに設定する事のできるプロパティは次の通りです。
- capture
前述同様にキャプチャリング対象か否かのboolean値を設定し、キャプチャリングか、バブリングかの設定をします。
- once
boolean値で設定し、trueを設定すると一回実行された後にイベントリスナが削除されます。
- passive
boolean値で設定し、trueを設定するとハンドラがpreventDefault()を呼び出さないことを設定します。trueの値が設定してある時に、preventDefault()が呼び出されてもその処理は行われません。
このオプションが存在する詳しい理由はMDNのページに載っていますが、preventDefaultが呼ばれる可能性があるとスクロール時にブラウザのレンダリングが遅くなるからだそうです。
現在のChromeやFireFoxでは、これに関する最適化がおこなわれていて設定しなくてもtrueとなることがあるとも書いてありました。
- signal
Abortシグナルを設定します。オブジェクトからabortメソッドが呼ばれるとリスナーは削除されます。
簡単な使い方は、AbortContollerのインスタンスを作成し、そのsignalプロパティをこのオブジェクトのプロパティに設定します。その後、AbortContollerのインスタンスからabortメソッドを呼ぶことでリスナーが削除されます。
abortメソッドにはメッセージを渡すことができますが、この場合は設定しても特に機能しません。
sample.html
<div id="outer" style="margin: 5px; background:#ccc"> outer(キャプチャリング) <div id="inner" style="margin: 5px; background:#ccf"> inner(バブリング) <div id="target" style="margin: 5px; background:#cfc"> target(キャプチャリング) </div> </div> </div> <button id="stop-listener">stop</button> <script> const controller = new AbortController(); document.getElementById("outer").addEventListener("click",() => { alert("outer");}, { capture: true, signal: controller.signal}); document.getElementById("inner").addEventListener("click",() => { alert("innerr");}, false); document.getElementById("target").addEventListener("click",() => { alert("target");}, true); document.getElementById("stop-listener").addEventListener("click",() => { controller.abort('abort message');}); </script>
その他メモ書き
イベントに対するデフォルトの挙動を止めたい場合にはevent.preventDefault、伝播を止めたい場合はevent.stopPropagationを呼びます。
Chromeのデベロッパーツールでは、イベントリスナーの一覧がだせるので、JavaScriptでも出せないかと調べてみたのですが、出せないようです。ただしデベロッパーツールのコンソール上ではgetEventListeners(DOM)とすると、イベントリスナの一覧が返ります。