82
72

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

本当は深いaddEventListener

Last updated at Posted at 2023-05-07

こんにちは。2年目に突入した新卒フロントエンドエンジニアです。

使用率は高い割に、
「なんかボタン押す時とか画面のリサイズ・ロードするときに使うやつでしょー」
くらいの知識で使っていた addEventListener() メソッドについて向き合うことにしました。

基本

JS
const button = document.querySelector('.button');

button.addEventListener('click', () => {
  /* ボタンをクリックした時の処理 */
});

基本的な addEventListener() の使い方です。window, document, bodyだけでなくあらゆる要素にイベント処理を登録することができます。死ぬほどよく見ますね。

event-listeners-01.png
登録したイベント情報は開発ツールで確認できます。写真はChromeのEvent Listenersタブです。

on~とaddEventlistener

JavaScriptでイベントを登録する方法にはもう一つ、on~がありますが、on~は一つのターゲットに複数のイベントが登録することができず、2つ目のon~イベントを登録すると上書きされてしまいます。

JS
const button = document.querySelector('.button');

button.onclick = () => {
  console.log('clicked from on*');
};
button.onclick = () => {
  console.log('clicked again from on*');
};
// log): clicked again from on*

buttonをクリックしても2回目のリスナーしか呼び出されません。


addEventListener() は複数登録することができます(実行は登録順)。

JS
const input = document.querySelector('.input');

// 関数化して複数登録(イベントターゲットが同じ時のみ)
const eventTypes = ['focus', 'input', 'submit'];
const entryAddEventListenerMulti = (target, types, handler, useCapture) => {
  for (let type of types) {
    target.addEventListener(type, handler, useCapture);
  }
};

entryAddEventListenerMulti(input, eventTypes, (event) => {
  /* input要素への処理 */
});

基本的には addEventListener() を使って古のブラウザ対応をしないといけない時などにon*を使うのがいいと思います。

eventを一回だけ実行したいとき

第3引数onceを指定することでeventを1回実行した後削除してくれます。

JS
const button = document.querySelector('.button');

button.addEventListener('click', () => {
  alert('clicked!');
}, {once: true});

event.preventDefault()

event.preventDefault()を使用してターゲットのデフォルトの動作(<a>タグのリンク遷移・フォームの送信など)を防止することができます。

JS
button.addEventListener('click', (event) => {
  event.preventDefault();
});

イベントハンドラ内のthis

イベントハンドラ内のthisはイベントターゲット要素を参照します。

JS
const button = document.querySelector('.button');

button.addEventListener('click', function() {
  console.log(this);
});
// log): <button class="button" type="button"></button>


アロー関数式ではevent.currentTargetプロパティで同じようにイベントターゲットを参照することができます。

JS
const button = document.querySelector('.button');

button.addEventListener('click', (event) => {
  console.log(event.currentTarget);
});
// log): <button class="button" type="button"></button>


bindを使えばthisの参照先を指定できます。

JS
const data = {
  type: 'hoge',
  name: 'hoge',
  method: function() {}
};

const handleClick = function() {
  console.log(this);
};

button.addEventListener('click', handleClick.bind(data));
// log): {type: 'hoge', name: 'hoge', method: ƒ} (Chrome Dev Tools)

thisの値はdataオブジェクトを参照します。

イベント伝達フェーズについて

イベント処理の伝達にはフェーズがあって、Windowからターゲットへ下りていくキャプチャリングフェーズ(1)、ターゲットに到達した時のターゲットフェーズ(2)、ターゲットからWindowに向けて上がっていくバブリングフェーズ(3)の3段階になっています。急に難しいです。

通常、イベントの実行はバブリングフェーズで、イベントのターゲットからWindowに向けて上がっていきながら実行されます。なので基本的にはこんな仕組みなんだなーと思っておけば問題ないと思います。
eventflow.png
([出典]JAVASCRIPT.INFO 現代の JavaScript チュートリアル -バブリング と キャプチャリング: https://ja.javascript.info/bubbling-and-capturing

JS
const input = document.querySelector('.input');

document.body.addEventListener('input', () => {
  console.log('input from body');
});
input.addEventListener('input', () => {
  console.log('input from input');
});
window.addEventListener('input', () => {
  console.log('input from window');
});
document.addEventListener('input', () => {
  console.log('input from document');
});
/* log:) input from input
*        input from body
*        input from document
*        input from window  */

記述順に関係なくinput→body→document→windowの順に実行されます。

キャプチャリングフェーズでイベントを実行したいとき

イベントリスナーの第3引数captureを指定してキャプチャリングフェーズでイベントを実行することもできます。

windowなどの祖先要素のイベントを優先的に発火することができますが、イベント処理順の予測が難しくなってしまうので基本的には使用しない方がいいと思います。

JS
document.addEventListener('input', () => {
  console.log('input from document');
}, {capture: true});

// trueだけでも指定できる
document.addEventListener('input', () => {
  console.log('input from document');
}, true);

☝🏻 もともとaddEventListenerにはuseCaptureオプションしか無かったが新たにオプションが追加されたため後方互換性のためにtrueのみでもuseCapture オプションが指定できるようになっているみたいです。

event.eventPhaseプロパティ

event.eventPhaseプロパティで現在のフェーズを確認することもできます

JS
document.addEventListener('input', (event) => {
  console.log(event.eventPhase);
}, {capture: true});
// log): 1

キャプチャリング= 1, ターゲット= 2, バブリング= 3 が返されます。

passiveによる性能の改善

preventDefault()をしていないときに、passiveオプションをtrueにすることでスクロールの処理性能の低下を防ぐことができます。

JS
window.addEventListener('scroll', () => {
  /* スクロール時の処理 */
}, {passive: true});

※ Safari 以外のブラウザーでは、WindowDocumentDocument.bodyに対する wheelmousewheeltouchstarttouchmoveイベントのpassiveオプションの既定値が true になっているようです。

主要なイベント一覧

よく使うイベントをざっとまとめてみました。

マウス

イベント 発生条件
mousedown / mouseup 要素上でマウスボタンがクリックされた / 離されたとき
mouseover / mouseout マウスポイントが要素に来る / 出ていったとき
mousemove 要素上でのマウス移動毎に発生
pointerdown / pointerup ポインターがアクティブ / 非アクティブになったとき
pointerover / pointerout ポインターが境界内 / 境界外に移動したとき
pointermove 要素上でのポインター移動毎に発生
click mousedownイベントの後に発生

☝🏻 pointer系イベントはmouse系イベントを継承していて、タッチペンや画面を直接タッチして操作するデバイスにも対応しているので、マウス系イベント実装の際はpointer系にしてしまって大丈夫そうです。(MDN

キーボード

イベント 発生条件
keydown キーを押したとき(長押しの場合は自動的に繰り返される)
keyup キーを離したとき

スクロール

イベント 発生条件
scroll ビューまたは要素がスクロールされたとき
wheel ユーザーがポインティングデバイス (通常はマウス) のホイールボタンを回転させたとき
scrollend 要素のスクロールが完了したとき

☝🏻 scrollend イベントは実験的な機能でChrome Canary、Chrome115以降、Firefox109以降のブラウザのみ対応しています。(Can I use)…2023/5/7時点

スクロールが完全に終了したことを検知できるので、スムーススクロールの終了を待ってDOM要素を操作したい時などに便利そうです。早く使えるようになってほしいですね…

フォーム関連

イベント 発生条件
focus 要素がフォーカスを受け取ったとき
blur 要素がフォーカスを失ったとき
focusin 要素がフォーカスを受け取ろうとしているとき
focusとの主な違いはバブリングを行わないこと
focusout 要素がフォーカスを失おうとしているとき
blurとの主な違いはバブリングを行わないこと
change <input>, <select>, <textarea> 要素において、ユーザーによる要素の値の変更が確定したとき
テキストの場合はフォーカスが外れたら発生する
input <input>, <select>, <textarea> の各要素の値 (value) が変更されたとき
changeとは違い入力があったら即座に発生する
submit フォームが送信されたとき

読み込み

イベント 条件
load ブラウザがすべてのリソース(画像, スタイルなど)を読み込んだとき
DOMContentLoaded ブラウザがHTMLを完全に読み込み、DOMツリーが構築されたとき
beforeunload / unload ユーザがページを離れようとしているとき

その他のイベント

イベント 発生条件
cut / copy / paste ClipboardEventクラスに属しており、コピー/ペーストされるデータへのアクセスを提供する
visibilitychange タブのコンテンツが表示状態または非表示状態になったときにdocument に発生
fullscreenchange ブラウザーが全画面モードに移行したり終了したりした直後に発生
resize ビュー (ウィンドウ) の大きさが変更されたとき
windowオブジェクトでのみ発行される
hashchange URL のフラグメント識別子 (URLの#記号で始まり続く部分) が変化したときに発生します。

他にもモバイルデバイス用のtouch系イベント(Safari非対応🥲)、ユーザーのドラッグを判定するdrag系イベント、CSSアニメーションの実行に合わせて発生するanimation 系イベント…などなど多くのイベントが用意されています。

W3C UI Events

カスタムイベントの作成

Event / CustomEventで独自のイベントを作成することもできます。

作成したイベントはdispatchEvent()で実行する必要があります。

JS
const myEvent = new Event('my-event');
document.dispatchEvent(myEvent);

カスタムイベントの特徴

自作のイベントは下記のような特徴を持っています

  • 自作のイベントはevent.isTrustedプロパティの値がfalseになる(本来のイベントはtrue)
  • event.bubblesプロパティの値がデフォルトでfalseになっている(バブリングしない)
  • event.cancelableプロパティの値がデフォルトでfalseになっている(preventDefaultできない)
  • event.detailに追加プロパティを設定できる↓
JS
// detailに追加プロパティを設定
document.addEventListener('my-event', (event) => {
  console.log(event.detail.message);
});

const myEvent = new CustomEvent('my-event', {
  bubbles: true,
  cancelable: true,
  detail: {
    message: 'This is Custom Event'
  }
});

document.dispatchEvent(myEvent);
// log): This is Custom Event

また上記のようにbubbles: trueを指定してバブリングさせる、cancelable: trueを指定してpreventDefault()を有効にすることもできます。


Event / CustomEventの代わりにMouseEventKeyboardEventFocusEventなどを使用してコンストラクタを作成できます。これらを使うことでコンストラクタの作成時にそれぞれのイベントの標準プロパティを指定することができます。

デフォルトのイベント’click’’keydown’などと同じ名前のイベントを作成することもできますが、その際は注意が必要です。

参考記事

最後に参考にした記事を載せます。見ていただいてありがとうございました🙇🏻‍♂️

82
72
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
82
72

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?