JavaScriptのイベントをたくさん見られるサイトを作る

JavaScriptのイベント、ややこしいですよね。

名前が似ているものや、使い方にクセがあるものもあり、簡単なようで実装に手間どることが多いです。なので、よく使うイベントの概要をざっくりまとめ、それぞれの実行タイミングとイベントオブジェクトをコンソールで確認できるサイトを作りました。

忘れたときに調べればいいだけの話なので、わざわざサイトを作る必要はなかったのですが、イベントを眺めるのが好きな人に見ていただけたら嬉しいです。


[<img src="https://qiita-image-store.s3.amazonaws.com/0/89103/2173f717-84d3-5fb1-cca1-90dcefa32472.gif" width="500">](https://listener.noplan.cc)

[JavaScriptのイベントをたくさん見られるサイト](https://listener.noplan.cc)


ひたすらaddEventListenerしていたら、イベントに愛着が湧いたので、イベントの実装でハマりそうなところをまとめています。

# なぜイベントを使うのか
マウスなどはイベントじゃないと値が取得できませんが、頑張ればイベントを使わずとも実装できるものもあります。たとえば、テキストボックスの変更を知りたいだけであれば、値を頻繁に確認し続ければ実装できなくはありません。しかしこれは明らかに資源の無駄遣いです。この例は極端ですが、イベントを使えば、値が変更したときに知らせてくれるので、そこらへんの無駄がなく実装することができます。

```javascript
const input = document.getElementById('input')

// 1秒に10回ぐらいテキストの値を確認し続ける
setInterval(() => {
  console.log(input.value)
}, 100)

// テキストが変更されたら実行される。嬉しい。
input.addEventListener('input', e => {
 console.log(input.value)
})
```

# イベントのややこしいところ
## イベントオブジェクト（e）
イベントの実行時によく `e` などの変数名で渡されるイベントオブジェクトというものがあります。このオブジェクトにどんなプロパティがあるかを忘れてしまうことが多いので、その都度調べたりConsoleで確認したりしますが、基本的には[Event](https://developer.mozilla.org/ja/docs/Web/API/Event)というインターフェースをベースにして、[MouseEvent](https://developer.mozilla.org/ja/docs/Web/API/MouseEvent)や[KeyboardEvent](https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent)などの派生したものがあるだけなので、思いのほかシンプルです。

よく使うイベントのインターフェースを表にまとめました。

| インターフェース | 継承 | プロパティ/メソッドの例 |
|:---|:---|:---|:---|
| [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event) | - | [target](https://developer.mozilla.org/en-US/docs/Web/API/Event/target), [preventDefault](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault) |
| [MouseEvent](https://developer.mozilla.org/ja/docs/Web/API/MouseEvent) | [Event](https://developer.mozilla.org/ja/docs/Web/API/Event) / [UIEvent](https://developer.mozilla.org/ja/docs/Web/API/UIEvent) | [clientX](https://developer.mozilla.org/ja/docs/Web/API/MouseEvent/clientX), [clientY](https://developer.mozilla.org/ja/docs/Web/API/MouseEvent/clientY) |
| [KeyboardEvent](https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent) | [Event](https://developer.mozilla.org/ja/docs/Web/API/Event) / [UIEvent](https://developer.mozilla.org/ja/docs/Web/API/UIEvent) | [key](https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/key), [shiftKey](https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/shiftKey) |
| [AnimationEvent](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent) | [Event](https://developer.mozilla.org/ja/docs/Web/API/Event) | [animationName](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent/animationName), [elapsedTime](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent/elapsedTime) |


## targetとcurrentTarget
イベントオブジェクトの中には、[target](https://developer.mozilla.org/ja/docs/Web/API/Event/target)と[currentTarget](https://developer.mozilla.org/ja/docs/Web/API/Event/currentTarget)という似たようなプロパティがあります。

> イベントが発生した要素を特定する event.target とは対照的に、常にイベントハンドラがアタッチされた要素を参照します。
>
> [Event.currentTarget | MDN](https://developer.mozilla.org/ja/docs/Web/API/Event/currentTarget)

イベントが発生した要素が `target`で、イベントを設定した要素が `currentTarget` になっています。多くの場合、これらは同じになりますが、子要素でイベントが発生した場合などに異なるときがあります。下に貼り付けているCodepenで子要素をクリックした場合、子要素( `target` )で発生したイベントがバブリングして、イベントを設定した要素( `currentTarget` )へ到達しているため、 `target` と `currentTarget` が異なります。

<p data-height="360" data-theme-id="0" data-slug-hash="bjjEYB" data-default-tab="css,result" data-user="noplan1989" data-pen-title="target or currentTarget" class="codepen">See the Pen <a href="https://codepen.io/noplan1989/pen/bjjEYB/">target or currentTarget</a> by noplan1989 (<a href="https://codepen.io/noplan1989">@noplan1989</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

この挙動は、イベントのキャプチャリングとバブリングを頭に入れて使わないとハマるかもしれません。バブリングとキャプチャリングについては、上手い絵を描く自信がなかったので、わかりやすかったMDNのページでご確認を。

[イベントのバブリングとキャプチャリング | MDN](https://developer.mozilla.org/ja/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture)

## リスナーの中のthis
JavaScriptとthisについてはもう語り尽くされているとは思いますが、イベントリスナーにおいてもハマるポイントになります。リスナーにふつうの関数(アローじゃない)を設定した場合、 `this` に 要素への参照（`currentTarget`）がバインドされています。これを意図して使う場合にはありがたいのですが、リスナーの外の `this` を使いたい場合は、 `that` や `self` などの変数に `this` を逃がす方法が使われていました。

モダンなブラウザでは、[アロー関数](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/arrow_functions)という、 `this` を束縛しない関数を使えるので、 苦しい実装からは解放されます。要素への参照が欲しい場合は、 `e.currentTarget` で取得できるので、思い通りにイベントを扱うことができて嬉しいですね。

[アロー関数](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/arrow_functions)はIEをはじめとした古いブラウザには対応していないため、[babel](https://babeljs.io/)でトランスパイルする必要があります。

```javascript
const obj = {
  hoge: function() {
    return 'hoge'
  },
  listen: function() {
    var self = this
    
    // ふつうの関数
    document.getElementById('hoge').addEventListener('click', function(e) {
      console.log(this)        // e.currentTarget
      console.log(this.hoge()) // this.hoge is not a function

      // 今は昔
      console.log(self)        // obj
      console.log(self.hoge()) // hoge
    })

    // アロー関数
    document.getElementById('hoge').addEventListener('click', e => {
      console.log(this)        // obj
      console.log(this.hoge()) // hoge
      
      console.log(e.currentTarget) // イベントを設定した要素への参照
    })
  }
}

obj.listen()
```

## 無名関数とremoveEventListener
removeすることを考えずに適当にaddしているとハマります。

```javascript
// addするぞ！
window.addEventListener('scroll', () => {
  console.log(window.scrollY)
})

// 不要になったから削除したい・・・気合だ！ => 無理
window.removeEventListener('scroll', () => {
  console.log(window.scrollY)
})
```

（誰もこんなことしないと思いますが）`anonymous` なものを `remove` しようなんてのは、どだい無理な話です。苦し紛れに同じ内容の無名関数で `remove` しようと試みても、別の関数なので思い通りには削除できません。

素直に名前をつけましょう。

```javascript
const handleScroll = () => {
  console.log(window.scrollY)
}

window.addEventListener('scroll', handleScroll)
window.removeEventListener('scroll', handleScroll)
```

もしかしたら、リスナーに引数を渡したくて、イベントオブジェクトも必要なときがあるかもしれません。そんなときは、イベントオブジェクトを引数に持つ関数を作ってあげることで解決できます。（もっといい方法があるかもしれませんが）

```javascript
const handleScroll = param => e => {
  console.log(param)
  console.log(e)
}

const callback = handleScroll('hoge')

window.addEventListener('scroll', callback)
window.removeEventListener('scroll', callback)
```

## 頻度が高いイベントへの対応
イベントには `click` のように扱いやすいものがある一方で、 `scroll` や `mousemove` などのように大量に実行されてしまうものもあります。そのようなイベントをそのまま実装してしまうと不要な呼び出しが多く、動作が重たくなる原因になってしまうので、適度に間引いて処理をしたほうがブラウザに優しいです。

最近のブラウザでは、指定した関数を描画の前に呼び出してくれる[requestAnimationFrame](https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame)という便利なメソッドがあるので、それを使用して最適化する例をいくつか載せています。

### スクロール

```javascript
let isRunning = false

window.addEventListener('scroll', () => {
  // 呼び出されるまで何もしない
  if (!isRunning) {
    isRunning = true

    // 描画する前のタイミングで呼び出してもらう
    window.requestAnimationFrame(() => {

      // ここでなにかする
      console.log(window.scrollY)

      isRunning = false
    })
  }
})
```

<p data-height="360" data-theme-id="0" data-slug-hash="gjGqvr" data-default-tab="result" data-user="noplan1989" data-pen-title="rAF scroll" class="codepen">See the Pen <a href="https://codepen.io/noplan1989/pen/gjGqvr/">rAF scroll</a> by noplan1989 (<a href="https://codepen.io/noplan1989">@noplan1989</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

### マウスムーブでドラッグ

```javascript
let isDragging = false
let startPosition = null
let lastPosition = null
let requestId = null

// ドラッグしたい要素
const circle = document.getElementById('circle')

const translate = (x, y) => {
  circle.style.transform = `translate3d(${x}px, ${y}px, 0)`
}

// この関数で描画する
const animate = () => {
  const x = lastPosition.x - startPosition.x
  const y = lastPosition.y - startPosition.y
  
  translate(x, y)
  
  requestId = window.requestAnimationFrame(animate)
}

// 最後にリセット
const leave = () => {
  isDragging = false
  translate(0, 0)

  // もう呼んでほしくないのでキャンセル
  if (requestId) {
    window.cancelAnimationFrame(requestId)
  }
}

// マウスが押されたらアニメーション開始
circle.addEventListener('mousedown', e => {
  isDragging = true
  startPosition = { x: e.clientX, y: e.clientY }
  lastPosition = { x: e.clientX, y: e.clientY }

  animate()
})

// 動いてるときは座標を更新
circle.addEventListener('mousemove', e => {
  if (isDragging) {
    lastPosition = { x: e.clientX, y: e.clientY }
  }
})

// 離れたらもとに戻す
circle.addEventListener('mouseup', e => {
  leave()
})

circle.addEventListener('mouseleave', e => {
  leave()
})
```

<p data-height="360" data-theme-id="0" data-slug-hash="mjBaGo" data-default-tab="result" data-user="noplan1989" data-pen-title="simple mouse drag" class="codepen">See the Pen <a href="https://codepen.io/noplan1989/pen/mjBaGo/">simple mouse drag</a> by noplan1989 (<a href="https://codepen.io/noplan1989">@noplan1989</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

今では少ないと思いますが、[requestAnimationFrame](https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame)に対応していない、IE9以前/Android4.3以前などのブラウザにも対応する必要がある場合は、非対応のときにsetTimeoutに置き換えてくれる[Polyfill](https://gist.github.com/paulirish/1579671)のようなもの使うか、別の実装を検討する必要があります。

[requestAnimationFrame | Can I use](https://caniuse.com/#feat=requestanimationframe)

ここでは `requestAnimationFrame` を使っていますが、それ以外の実装が適している場合もあります。`resize` の場合は、 連続している場合は実行させない `debounce` の方が適切なこともあると思うので、イベントや状況に応じて適切な間引き処理を行わなければなりません。 `debounce` や `throttle ` についてわかりやすいページがあったのでリンクを張らせてもらいます。

[JavaScriptでの多発するイベントの間引き処理 | つみきブログ](https://blog.tsumikiinc.com/article/20141125_javascript-event-throttle.html)

# 似たようなイベントの違い
## mouseenterとmouseover
名前を見ただけでは、いまいちよくわかりませんが、違いは子要素にも反応するかどうかです。 `mouseenter` はイベントを設定した要素のみに反応し、 `mouseover` はその子要素にも反応します。`mouseenter` に対する `mouseleave` と、 `mouseover` に対する `mouseout` も同様の挙動です。

[mouseenter](https://developer.mozilla.org/en-US/docs/Web/Events/mouseenter)
[mouseover](https://developer.mozilla.org/en-US/docs/Web/Events/mouseover)

<p data-height="360" data-theme-id="0" data-slug-hash="XBBjVY" data-default-tab="result" data-user="noplan1989" data-pen-title="mouseenter or mousemove" class="codepen">See the Pen <a href="https://codepen.io/noplan1989/pen/XBBjVY/">mouseenter or mousemove</a> by noplan1989 (<a href="https://codepen.io/noplan1989">@noplan1989</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## keydownとkeypress
`keydown` は、キーが押されたとき反応するのでわかりやすいですが、 `keypress` はShiftやAltなどの修飾キーや、IMEの入力には反応せず、値を変化させるキー入力のみに反応します。

[keydown](https://developer.mozilla.org/en-US/docs/Web/Events/keydown)
[keypress](https://developer.mozilla.org/en-US/docs/Web/Events/keypress)

## inputとchange
テキストボックスの場合、 `input` は値が変わるごと反応しますが、 `change` はフォーカスが外れて値の変更が確定したときに反応します。実際の挙動は、今回作ったサイトでコンソールを開いてご確認ください。

[フォーム | JavaScriptのイベントをたくさん見られるサイト](https://listener.noplan.cc/form/)

# サイトを作るときに諦めたところ
## drop系のイベント
面倒だったのでスルーしました。そのうち追加するかもしれません。
mouseやtouchで代用できるので使ったことがないのですが、実際にどのくらい使われているのだろうか。

## 開発ツールが別ウィンドウで開かれたときに検出できない
大切なことはすべてコンソールに出力することにしたので、開発ツールが開かれていないときにはメッセージを出すようにしました。開発ツールが開いているかの判定には、[devtools-detect](https://github.com/sindresorhus/devtools-detect)というライブラリを使わせてもらっています。コードを確認すると、[window.outerWidth](https://developer.mozilla.org/ja/docs/Web/API/window.outerWidth)と[window.innerWidth](https://developer.mozilla.org/ja/docs/Web/API/Window/innerWidth)の違いなどをもとに判定しているようなのですが、開発ツールを別ウィンドウで開かれてしまった場合には、その判定では対応できません。

https://github.com/sindresorhus/devtools-detect/blob/gh-pages/index.js

いろいろと探したり考えたりしたのですが、思いつかなかったのでそのままにしました。

## テスト
要素のイベントをひたすらリッスンするサイトなのでテストがつらい、というか労力に見合わない気がしたのでブラウザ上でエラーが出ていないのを確認するだけで済ませました。

# できあがり

[<img src="https://qiita-image-store.s3.amazonaws.com/0/89103/2173f717-84d3-5fb1-cca1-90dcefa32472.gif" width="500">](https://listener.noplan.cc)

[JavaScriptのイベントをたくさん見られるサイト](https://listener.noplan.cc)

**ソースコード**
https://github.com/noplan1989/javascript-listener

ひたすらイベントを追加する作業だったので単調でつらかったのですが、とくに好きでも嫌いでもなかったイベントに対して謎の愛着が湧いたので、わざわざ作った甲斐がありました。あと、[Nuxt.js](https://ja.nuxtjs.org/)が使いやすかったおかげで、想定していたよりも早くできたのが嬉しかったです。

最後に、単調な作業にぬくもりを与えてくれた、合唱のプレイリストを貼っておきます。
https://open.spotify.com/user/y7ue1m2wfv8dmgivarhijmh6q/playlist/1rm6mori7LnjKNkxVfm1dl