本当は深いaddEventListener




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

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


## 基本

```javascript:JS
const button = document.querySelector('.button');

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

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

![event-listeners-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2612989/d8f414e3-b71e-6ba6-ee88-1e51edcb557a.png)
登録したイベント情報は開発ツールで確認できます。写真はChromeのEvent Listenersタブです。

## on~とaddEventlistener

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

```javascript: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をクリックしても２回目のリスナーしか呼び出されません。

<br>**addEventListener()** は複数登録することができます（実行は登録順）。

```javascript: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回実行した後削除してくれます。

```javascript:JS
const button = document.querySelector('.button');

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

## event.preventDefault()

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

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

## イベントハンドラ内のthis

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

```javascript:JS
const button = document.querySelector('.button');

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

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

```javascript:JS
const button = document.querySelector('.button');

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

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

```javascript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2612989/a6c061f5-1145-1535-827d-f57889385ab2.png)
（[出典]JAVASCRIPT.INFO 現代の JavaScript チュートリアル -バブリング と キャプチャリング: [https://ja.javascript.info/bubbling-and-capturing](https://ja.javascript.info/bubbling-and-capturing)）


```javascript: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などの祖先要素のイベントを優先的に発火することができますが、イベント処理順の予測が難しくなってしまうので基本的には使用しない方がいいと思います。

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

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

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

### event.eventPhaseプロパティ

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

```javascript:JS
document.addEventListener('input', (event) => {
  console.log(event.eventPhase);
}, {capture: true});
// log): 1
```
キャプチャリング= 1, ターゲット= 2, バブリング= 3 が返されます。

## passiveによる性能の改善

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

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

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

## 主要なイベント一覧

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

### マウス

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

:::note info
☝🏻 `pointer`系イベントは`mouse`系イベントを継承していて、タッチペンや画面を直接タッチして操作するデバイスにも対応しているので、マウス系イベント実装の際は`pointer`系にしてしまって大丈夫そうです。（[MDN](https://developer.mozilla.org/ja/docs/Web/API/Pointer_events)）
:::

### キーボード

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

### スクロール

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

:::note warn
☝🏻 `scrollend` イベントは実験的な機能でChrome Canary、Chrome115以降、Firefox109以降のブラウザのみ対応しています。（[Can I use](https://caniuse.com/?search=scrollend)）…2023/5/7時点

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

### フォーム関連

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

### 読み込み

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

### その他のイベント

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

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

#### W3C UI Events

https://www.w3.org/TR/uievents/#legacy-UIEvent


## カスタムイベントの作成

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

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

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

### カスタムイベントの特徴

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

- 自作のイベントは`event.isTrusted`プロパティの値がfalseになる（本来のイベントはtrue）
- `event.bubbles`プロパティの値がデフォルトでfalseになっている（バブリングしない）
- `event.cancelable`プロパティの値がデフォルトでfalseになっている（preventDefaultできない）
- `event.detail`に追加プロパティを設定できる↓

```javascript: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()を有効にすることもできます。

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

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

## 参考記事

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

- [イベント操作する上で押さえておきたいポイント(addEventListener)](https://www.wakuwakubank.com/posts/607-javascript-event/)
- [イベントを一度だけ実行して破棄する方法](https://1-notes.com/javascript-run-and-remove-the-event-only-once/)
- [イベントのthisとbind](https://se-tomo.com/2019/03/09/%E3%80%90javascript%E3%80%91%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%AEthis%E3%81%A8bind/)
- [バブリング と キャプチャリング](https://ja.javascript.info/bubbling-and-capturing)
- [touchやwheelのイベントリスナーにpassive: trueを明示的に指定する必要があるのか調べてみた](https://qiita.com/kyntk/items/5c16846a9b0e0b36154c)
- [【JavaScript】スクロールが終わった時に発生する「新しいイベントScrollend」とは？](https://qiita.com/degudegu2510/items/586d2713c30a3ef9b312)
- [カスタムイベントのディスパッチ](https://ja.javascript.info/dispatch-events#ref-112)