JavaScriptのイベント、ややこしいですよね。
名前が似ているものや、使い方にクセがあるものもあり、簡単なようで実装に手間どることが多いです。なので、よく使うイベントの概要をざっくりまとめ、それぞれの実行タイミングとイベントオブジェクトをコンソールで確認できるサイトを作りました。
忘れたときに調べればいいだけの話なので、わざわざサイトを作る必要はなかったのですが、イベントを眺めるのが好きな人に見ていただけたら嬉しいです。
ひたすらaddEventListenerしていたら、イベントに愛着が湧いたので、イベントの実装でハマりそうなところをまとめています。
なぜイベントを使うのか
マウスなどはイベントじゃないと値が取得できませんが、頑張ればイベントを使わずとも実装できるものもあります。たとえば、テキストボックスの変更を知りたいだけであれば、値を頻繁に確認し続ければ実装できなくはありません。しかしこれは明らかに資源の無駄遣いです。この例は極端ですが、イベントを使えば、値が変更したときに知らせてくれるので、そこらへんの無駄がなく実装することができます。
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というインターフェースをベースにして、MouseEventやKeyboardEventなどの派生したものがあるだけなので、思いのほかシンプルです。
よく使うイベントのインターフェースを表にまとめました。
| インターフェース | 継承 | プロパティ/メソッドの例 |
|:---|:---|:---|:---|
| Event | - | target, preventDefault |
| MouseEvent | Event / UIEvent | clientX, clientY |
| KeyboardEvent | Event / UIEvent | key, shiftKey |
| AnimationEvent | Event | animationName, elapsedTime |
targetとcurrentTarget
イベントオブジェクトの中には、targetとcurrentTargetという似たようなプロパティがあります。
イベントが発生した要素を特定する event.target とは対照的に、常にイベントハンドラがアタッチされた要素を参照します。
イベントが発生した要素が target
で、イベントを設定した要素が currentTarget
になっています。多くの場合、これらは同じになりますが、子要素でイベントが発生した場合などに異なるときがあります。下に貼り付けているCodepenで子要素をクリックした場合、子要素( target
)で発生したイベントがバブリングして、イベントを設定した要素( currentTarget
)へ到達しているため、 target
と currentTarget
が異なります。
See the Pen target or currentTarget by noplan1989 (@noplan1989) on CodePen.
この挙動は、イベントのキャプチャリングとバブリングを頭に入れて使わないとハマるかもしれません。バブリングとキャプチャリングについては、上手い絵を描く自信がなかったので、わかりやすかったMDNのページでご確認を。
リスナーの中のthis
JavaScriptとthisについてはもう語り尽くされているとは思いますが、イベントリスナーにおいてもハマるポイントになります。リスナーにふつうの関数(アローじゃない)を設定した場合、 this
に 要素への参照(currentTarget
)がバインドされています。これを意図して使う場合にはありがたいのですが、リスナーの外の this
を使いたい場合は、 that
や self
などの変数に this
を逃がす方法が使われていました。
モダンなブラウザでは、アロー関数という、 this
を束縛しない関数を使えるので、 苦しい実装からは解放されます。要素への参照が欲しい場合は、 e.currentTarget
で取得できるので、思い通りにイベントを扱うことができて嬉しいですね。
アロー関数はIEをはじめとした古いブラウザには対応していないため、babelでトランスパイルする必要があります。
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しているとハマります。
// addするぞ!
window.addEventListener('scroll', () => {
console.log(window.scrollY)
})
// 不要になったから削除したい・・・気合だ! => 無理
window.removeEventListener('scroll', () => {
console.log(window.scrollY)
})
(誰もこんなことしないと思いますが)anonymous
なものを remove
しようなんてのは、どだい無理な話です。苦し紛れに同じ内容の無名関数で remove
しようと試みても、別の関数なので思い通りには削除できません。
素直に名前をつけましょう。
const handleScroll = () => {
console.log(window.scrollY)
}
window.addEventListener('scroll', handleScroll)
window.removeEventListener('scroll', handleScroll)
もしかしたら、リスナーに引数を渡したくて、イベントオブジェクトも必要なときがあるかもしれません。そんなときは、イベントオブジェクトを引数に持つ関数を作ってあげることで解決できます。(もっといい方法があるかもしれませんが)
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という便利なメソッドがあるので、それを使用して最適化する例をいくつか載せています。
スクロール
let isRunning = false
window.addEventListener('scroll', () => {
// 呼び出されるまで何もしない
if (!isRunning) {
isRunning = true
// 描画する前のタイミングで呼び出してもらう
window.requestAnimationFrame(() => {
// ここでなにかする
console.log(window.scrollY)
isRunning = false
})
}
})
See the Pen rAF scroll by noplan1989 (@noplan1989) on CodePen.
マウスムーブでドラッグ
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()
})
See the Pen simple mouse drag by noplan1989 (@noplan1989) on CodePen.
今では少ないと思いますが、requestAnimationFrameに対応していない、IE9以前/Android4.3以前などのブラウザにも対応する必要がある場合は、非対応のときにsetTimeoutに置き換えてくれるPolyfillのようなもの使うか、別の実装を検討する必要があります。
requestAnimationFrame | Can I use
ここでは requestAnimationFrame
を使っていますが、それ以外の実装が適している場合もあります。resize
の場合は、 連続している場合は実行させない debounce
の方が適切なこともあると思うので、イベントや状況に応じて適切な間引き処理を行わなければなりません。 debounce
や throttle
についてわかりやすいページがあったのでリンクを張らせてもらいます。
JavaScriptでの多発するイベントの間引き処理 | つみきブログ
似たようなイベントの違い
mouseenterとmouseover
名前を見ただけでは、いまいちよくわかりませんが、違いは子要素にも反応するかどうかです。 mouseenter
はイベントを設定した要素のみに反応し、 mouseover
はその子要素にも反応します。mouseenter
に対する mouseleave
と、 mouseover
に対する mouseout
も同様の挙動です。
See the Pen mouseenter or mousemove by noplan1989 (@noplan1989) on CodePen.
keydownとkeypress
keydown
は、キーが押されたとき反応するのでわかりやすいですが、 keypress
はShiftやAltなどの修飾キーや、IMEの入力には反応せず、値を変化させるキー入力のみに反応します。
inputとchange
テキストボックスの場合、 input
は値が変わるごと反応しますが、 change
はフォーカスが外れて値の変更が確定したときに反応します。実際の挙動は、今回作ったサイトでコンソールを開いてご確認ください。
フォーム | JavaScriptのイベントをたくさん見られるサイト
サイトを作るときに諦めたところ
drop系のイベント
面倒だったのでスルーしました。そのうち追加するかもしれません。
mouseやtouchで代用できるので使ったことがないのですが、実際にどのくらい使われているのだろうか。
開発ツールが別ウィンドウで開かれたときに検出できない
大切なことはすべてコンソールに出力することにしたので、開発ツールが開かれていないときにはメッセージを出すようにしました。開発ツールが開いているかの判定には、devtools-detectというライブラリを使わせてもらっています。コードを確認すると、window.outerWidthとwindow.innerWidthの違いなどをもとに判定しているようなのですが、開発ツールを別ウィンドウで開かれてしまった場合には、その判定では対応できません。
いろいろと探したり考えたりしたのですが、思いつかなかったのでそのままにしました。
テスト
要素のイベントをひたすらリッスンするサイトなのでテストがつらい、というか労力に見合わない気がしたのでブラウザ上でエラーが出ていないのを確認するだけで済ませました。
できあがり
ソースコード
https://github.com/noplan1989/javascript-listener
ひたすらイベントを追加する作業だったので単調でつらかったのですが、とくに好きでも嫌いでもなかったイベントに対して謎の愛着が湧いたので、わざわざ作った甲斐がありました。あと、Nuxt.jsが使いやすかったおかげで、想定していたよりも早くできたのが嬉しかったです。
最後に、単調な作業にぬくもりを与えてくれた、合唱のプレイリストを貼っておきます。
https://open.spotify.com/user/y7ue1m2wfv8dmgivarhijmh6q/playlist/1rm6mori7LnjKNkxVfm1dl