JavaScriptの基礎 - イベント(DOM)
概要
DOMイベントとは、Webブラウザ上で発生するユーザ操作やシステム上の出来事を検出して処理するための仕組みである。
クリック、キー入力、フォーム送信、ページ読み込み等の出来事がイベントとして発生し、登録されたリスナー関数が呼び出される。
イベントは EventTarget インターフェースを実装する全てのオブジェクトで発生する。
代表的な実装対象として、Element、Document、Window があり、XMLHttpRequest や AudioNode 等もイベントを送出する。
イベントの登録には、addEventListener メソッドを使用する。
イベントが発生すると、DOMツリーをキャプチャリングフェーズで下降して、ターゲットに到達した後にバブリングフェーズで上昇する。
この伝播の仕組みを利用して、親要素で子要素のイベントをまとめて処理するイベント委譲というパターンも広く使われる。
また、Reactではブラウザのネイティブイベントをラップした SyntheticEvent を使用しており、Webブラウザ間の差異を吸収している。
DOMの基本操作については、JavaScriptの基礎 - DOM操作のページを参照すること。
addEventListener
addEventListener は、指定したイベントタイプに対してリスナー関数を登録するメソッドである。
同じイベントタイプに対して複数のハンドラを登録でき、それぞれが独立して実行される。
基本構文
addEventListener の基本的な呼び出し形式を以下に示す。
element.addEventListener(type, listener)
element.addEventListener(type, listener, options)
element.addEventListener(type, listener, useCapture)
下表に、引数の説明を示す。
| 引数 | 説明 |
|---|---|
| 登録するイベントの種類を表す文字列 例: 'click', 'keydown', 'submit'
| |
listener |
イベント発生時に呼び出されるコールバック関数 引数としてイベントオブジェクトを受け取る。 |
options |
オプションオブジェクト または useCapture の真偽値 (省略可能)
|
使用例を以下に示す。
const button = document.getElementById('myButton')
button.addEventListener('click', (event) => {
console.log('クリックされました')
console.log(event.target)
})
オプション
第3引数にオブジェクトを渡すことにより、リスナーの動作を細かく制御できる。
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
capture |
Boolean | false | キャプチャフェーズでリスナーを実行するかどうかを指定する。 false の場合はバブリングフェーズで実行される。 |
once |
Boolean | false | true の場合、リスナーは最初のイベント発生時にのみ実行され、その後自動的に削除される。 |
passive |
Boolean | false | true の場合、preventDefault() を呼び出さないことをブラウザに宣言する。スクロールパフォーマンスの向上に活用できる。 |
signal |
AbortSignal | - | AbortSignal オブジェクトを指定することにより、abort() 呼び出し時にリスナーを自動的に削除できる。
|
onceオプション
once: true を指定すると、リスナーは最初のイベント発生時にのみ実行され、実行後に自動的に削除される。
手動で removeEventListener を呼び出す必要がないため、1回限りの処理に適している。
const button = document.getElementById('myButton')
// 最初の1回だけクリックを処理する
button.addEventListener('click', (event) => {
console.log('最初のクリックのみ処理されます')
}, { once: true })
passive オプション
passive: true を指定すると、リスナー内で preventDefault() を呼び出さないことをWebブラウザに宣言する。
Webブラウザはリスナーの実行を待たずにスクロール処理を即座に開始できるため、スムーズなスクロール体験を実現できる。
const container = document.getElementById('scrollContainer')
container.addEventListener('wheel', (event) => {
// スクロール関連の処理 (preventDefault()は呼ばない)
console.log('スクロール量:', event.deltaY)
}, { passive: true })
signal オプション
AbortController と AbortSignal を組み合わせることにより、複数のリスナーを1度にまとめて削除できる。
無名関数で登録したリスナーも削除できるため、関数参照を保持する必要がなくなる。
const controller = new AbortController()
element.addEventListener('click', () => {
console.log('クリックされました')
}, { signal: controller.signal })
element.addEventListener('keydown', () => {
console.log('キーが押されました')
}, { signal: controller.signal })
element.addEventListener('mousemove', () => {
console.log('マウスが移動しました')
}, { signal: controller.signal })
// 全てのリスナーを一度に削除する
controller.abort()
removeEventListener
removeEventListener は、addEventListener で登録したリスナーを削除するメソッドである。
基本的な使い方
removeEventListener でリスナーを削除するには、登録時と同じ関数参照を渡す必要がある。
ただし、無名関数を使用した場合は同一の参照が得られないため、削除できないことに注意が必要である。
// 正しい例 : 名前付き関数を使用する
function handleClick() {
console.log('クリックされました')
}
element.addEventListener('click', handleClick)
element.removeEventListener('click', handleClick) // 同じ参照なので削除される
// 動作しない例 : 無名関数を使用する
element.addEventListener('click', function() { console.log('clicked') })
element.removeEventListener('click', function() { console.log('clicked') }) // 別の関数参照なので削除されない
また、capture フラグが登録時と完全に一致していない場合も削除に失敗する。
element.addEventListener('mousedown', handleMouseDown, true)
element.removeEventListener('mousedown', handleMouseDown, true) // 成功
element.removeEventListener('mousedown', handleMouseDown, false) // 失敗 (captureが一致しない)
AbortControllerによる解除
signal オプションを活用した現代的なパターンでは、複数のリスナーをまとめて管理・解除できる。
コンポーネントのクリーンアップ処理等、複数のリスナーを一括削除する場面で有効である。
class EventManager {
constructor() {
this.controller = new AbortController()
}
setup(element) {
const signal = this.controller.signal
element.addEventListener('click', this.handleClick, { signal })
element.addEventListener('keydown', this.handleKeydown, { signal })
element.addEventListener('scroll', this.handleScroll, { signal })
}
handleClick(event) { console.log('クリック:', event.target) }
handleKeydown(event) { console.log('キー:', event.key) }
handleScroll(event) { console.log('スクロール') }
// 全てのリスナーを一括削除する
cleanup() {
this.controller.abort()
}
}
イベントオブジェクト
イベント発生時にリスナー関数へ渡されるオブジェクトである。
発生したイベントに関する詳細な情報とメソッドを提供する。
共通プロパティ
下表に、全てのイベントオブジェクトが持つ共通のプロパティを示す。
| プロパティ | 型 | 説明 |
|---|---|---|
type |
String | イベントの種類を識別する名前 例: 'click', 'keydown'
|
target |
EventTarget | イベントが最初にディスパッチされた要素 バブリング時も変わらない。 |
currentTarget |
EventTarget | 現在イベントリスナーが登録されている要素 バブリング中は変化する。 |
timeStamp |
DOMHighResTimeStamp | イベントが作成された時刻 (ページ表示開始からのミリ秒) |
eventPhase |
Number | イベントフローの現在フェーズ 1: キャプチャリング 2: ターゲット 3: バブリング |
bubbles |
Boolean | イベントがバブリングするかどうか |
cancelable |
Boolean | preventDefault() でデフォルト動作をキャンセルできるかどうか
|
defaultPrevented |
Boolean | preventDefault() が呼び出されたかどうか
|
isTrusted |
Boolean | Webブラウザにより生成されたイベントの場合は true スクリプトで生成した場合は false |
下表に、target と currentTarget の違いを示す。
| プロパティ | 説明 |
|---|---|
event.target |
イベントが最初に発火した実際の要素 子要素をクリックした場合はその子要素を指す。 |
event.currentTarget |
リスナーが登録された要素 バブリング中は親要素を指すこともある。 |
共通メソッド
全てのイベントオブジェクトが持つ主要なメソッドを以下に示す。
preventDefault
cancelable が true のイベントに対して、Webブラウザのデフォルト動作をキャンセルする。
フォームの送信、リンクのナビゲーション、テキスト選択等のデフォルト動作を制御する時に使用する。
// フォームのデフォルト送信をキャンセルして独自処理を行う
form.addEventListener('submit', (event) => {
event.preventDefault()
// 独自のバリデーションや非同期送信処理を実行
handleFormSubmit(new FormData(form))
})
// リンクのナビゲーションをキャンセルする
link.addEventListener('click', (event) => {
event.preventDefault()
handleNavigation(link.href)
})
stopPropagation
イベントのDOM内での伝播を停止する。
このメソッドを呼び出すと、親要素に向かうバブリングが停止する。
child.addEventListener('click', (event) => {
event.stopPropagation()
console.log('子要素のみで処理され、親要素には伝播しない')
})
stopImmediatePropagation
stopPropagation の効果に加えて、同じ要素に登録された後続のリスナーの実行も防止する。
element.addEventListener('click', (event) => {
event.stopImmediatePropagation()
console.log('このリスナーは実行される')
})
element.addEventListener('click', (event) => {
console.log('このリスナーは実行されない')
})
マウスイベントのプロパティ
下表に、マウスイベント固有のプロパティを示す。
| プロパティ | 説明 |
|---|---|
clientX / clientY |
ビューポート (表示領域) の左上を原点とした座標 スクロール位置に依存しない。 |
pageX / pageY |
ドキュメント全体の左上を原点とした座標 スクロール量を含む。 |
offsetX / offsetY |
イベントのターゲット要素のパディングエッジを原点とした座標 |
screenX / screenY |
ユーザの物理スクリーンの左上を原点とした座標 |
button |
押されたマウスボタンを示す数値 0 : 左ボタン 1 : 中ボタン 2 : 右ボタン click イベントでは常に 0
|
altKey / ctrlKey / shiftKey / metaKey |
イベント発生時に対応する修飾キーが押されていたかどうか |
キーボードイベントのプロパティ
キーボードイベント固有の重要なプロパティを以下に示す。
| プロパティ | 説明 | 例: 日本語キーボードで[Y]キーを押下した場合 |
|---|---|---|
key |
ユーザのキーボードレイアウトに依存した実際の文字 (論理キー) | "y" または "Y" |
code |
キーボードレイアウトに依存しない物理的なキーの位置 (物理キー) | "KeyY" (常に固定) |
下表に、使い分けの指針を示す。
| プロパティ | 使用する場面 |
|---|---|
key |
ユーザが何を入力しているかを判定したい場合 (テキスト入力の処理、ショートカットキーの検出等) |
code |
キーボードの物理的な位置が重要な場合 (ゲームのWASD操作等、レイアウトによらず同じキーを使用する場合) |
document.addEventListener('keydown', (event) => {
console.log(`key: ${event.key}`) // "y" または "z" (レイアウトに依存)
console.log(`code: ${event.code}`) // "KeyY" (常に物理位置)
// 修飾キーの組み合わせを検出する例
if (event.ctrlKey && event.key === 's') {
event.preventDefault()
saveDocument()
}
})
イベントバブリングとキャプチャリング
DOMイベントは要素間を伝播する仕組みを持つ。
この仕組みを理解することは、イベントハンドリングを正確に制御するために不可欠である。
3つのフェーズ
DOMイベントは以下の3つのフェーズを順番に経る。
- キャプチャリング段階 (
eventPhase: 1)- イベントはウィンドウから発生した要素 (ターゲット) に向かって伝播する。
capture: trueで登録したリスナーがこのフェーズで実行される。
- ターゲット段階 (
eventPhase: 2)- イベントがターゲット要素に到達した状態。
- ターゲット要素に登録されたリスナーが実行される。
- バブリング段階 (
eventPhase: 3)- イベントはターゲット要素から祖先要素に向かって伝播する。
- デフォルト (
capture: false) で登録したリスナーがこのフェーズで実行される。
バブリングの実例
ネストした要素においてバブリングがどのように機能するかを以下に示す。
const parent = document.getElementById('parent')
const child = document.getElementById('child')
// バブリングフェーズ (デフォルト) で登録
parent.addEventListener('click', () => {
console.log('Parent: バブリング')
})
child.addEventListener('click', () => {
console.log('Child: ターゲット')
})
// child要素をクリックした際の実行順序:
// 1. Child: ターゲット
// 2. Parent: バブリング
キャプチャリングの実例
capture: true を指定すると、バブリングフェーズより先にリスナーが実行される。
const parent = document.getElementById('parent')
const child = document.getElementById('child')
// キャプチャリングフェーズで登録
parent.addEventListener('click', () => {
console.log('Parent: キャプチャリング')
}, { capture: true })
child.addEventListener('click', () => {
console.log('Child: ターゲット')
})
// バブリングフェーズで登録
parent.addEventListener('click', () => {
console.log('Parent: バブリング')
})
// child要素をクリックした際の実行順序:
// 1. Parent: キャプチャリング
// 2. Child: ターゲット
// 3. Parent: バブリング
バブリングしないイベント
以下に示すイベントは bubbles プロパティが false であり、バブリングしない。
- focus
- 代替として focusin (バブリングする) を使用できる。
- blur
- 代替として focusout (バブリングする) を使用できる。
- mouseenter
- 対応する mouseover はバブリングする。
- mouseleave
- 対応する mouseout はバブリングする。
- load
- unload
- scroll
- resize
stopPropagationの注意点
stopPropagation を安易に使用すると、以下に示すような問題が生じる可能性がある。
- グローバルに登録したクリックハンドラ (モーダルの外側クリックで閉じる処理等) が動作しなくなる。
- アナリティクスツールや計測タグのイベントトラッキングが機能しなくなる。
- 他の機能が依存するイベントリスナーが動作しなくなる。
代替として、event.target を確認することにより、伝播を止めずに条件分岐できる。
// stopPropagationを使用する代わりに、targetを確認する
const modal = document.getElementById('modal')
modal.addEventListener('click', (event) => {
// モーダルの背景 (overlay) をクリックした場合のみ閉じる
if (event.target === modal) {
closeModal()
}
})
イベント委譲
イベント委譲 (Event Delegation) は、個々の子要素にリスナーを登録する代わりに、
親要素1つにリスナーを登録し、バブリングを利用してイベントを処理するパターンである。
概念と利点
イベント委譲の主なメリットを以下に示す。
- パフォーマンス向上
- 登録するリスナーの数を大幅に削減できる。
- 100件のリスト項目があっても、リスナーは1つで済む。
- 動的要素への対応
- 後から追加された要素に対しても自動的にリスナーが適用される。
- そのため、新たに登録し直す必要がない。
- コード量の削減
- 1つのハンドラで複数の要素のイベントを処理できる。
event.targetを使用したパターン
最もシンプルなイベント委譲の例を以下に示す。
const list = document.getElementById('list')
// リスト全体に1つのリスナーを登録する
list.addEventListener('click', (event) => {
// クリックされた要素が削除ボタンかどうかを確認する
if (event.target.classList.contains('btn-delete')) {
// 対象のリスト項目を削除する
event.target.closest('li').remove()
}
// 編集ボタンのクリックを処理する
if (event.target.classList.contains('btn-edit')) {
const item = event.target.closest('li')
startEditing(item)
}
})
event.target.closest()を使用したパターン
closest() メソッドを組み合わせることにより、ネストした要素構造でもより堅牢なイベント委譲を実装できる。
closest() の詳細については、JavaScriptの基礎 - DOM操作のページを参照すること。
document.getElementById('table-body').addEventListener('click', (event) => {
// クリックされた位置から最も近い .btn-delete を持つ要素を探す
const deleteBtn = event.target.closest('.btn-delete')
if (deleteBtn) {
// ボタンを含む行 (tr要素) を取得する
const row = deleteBtn.closest('tr')
const id = row.dataset.id
// サーバーへの削除リクエストを送信してから行を削除する
deleteRecord(id).then(() => {
row.remove()
})
}
// 編集ボタンの処理
const editBtn = event.target.closest('.btn-edit')
if (editBtn) {
const row = editBtn.closest('tr')
openEditDialog(row.dataset.id)
}
})
主要なイベント一覧
マウスイベント
| イベント | バブリング | cancelable | 説明 |
|---|---|---|---|
click |
○ | ○ | 要素をクリック (マウスボタンを押して離す) した時に発生する。 |
dblclick |
○ | ○ | 要素をダブルクリックした時に発生する。 |
mousedown |
○ | ○ | マウスボタンを押した瞬間に発生する。 |
mouseup |
○ | ○ | マウスボタンを離した瞬間に発生する。 |
mousemove |
○ | × | マウスポインタが移動した時に発生する。 |
mouseenter |
× | × | ポインタが要素に進入した時に発生する。 バブリングしない。 |
mouseleave |
× | × | ポインタが要素から離脱した時に発生する。 バブリングしない。 |
mouseover |
○ | × | ポインタが要素上に来た時に発生する。 子要素を含む。 バブリングする。 |
mouseout |
○ | × | ポインタが要素から離れた時に発生する。 子要素を含む。 バブリングする。 |
contextmenu |
○ | ○ | 右クリック時に発生する。preventDefault() でコンテキストメニューを抑制できる。
|
キーボードイベント
| イベント | バブリング | cancelable | 説明 |
|---|---|---|---|
keydown |
○ | ○ | キーを押した瞬間に発生する。 キーを押下し続けると繰り返し発生する。 |
keyup |
○ | ○ | キーを離した瞬間に発生する。 |
keypress |
○ | ○ | (非推奨) 文字キーを押した時に発生する。keydown または input イベントを使用すること。
|
keypress が非推奨となった理由を以下に示す。
- デバイスや入力方法への依存性が高く、IME (日本語入力等) で正しく機能しない場合がある。
- Webブラウザ間で動作にばらつきがある。
- テキスト入力の検出には、
inputイベント、キー操作の検出にはkeydownイベントを使用すること。
フォームイベント
| イベント | バブリング | cancelable | 説明 |
|---|---|---|---|
submit |
○ | ○ | フォームが送信された時に発生する。preventDefault() でページ遷移を防止できる。
|
change |
○ | × | 入力値が変更され、要素がフォーカスを失った時に発生する。 |
input |
○ | × | 入力値がリアルタイムに変更された時に発生する。 |
focus |
× | × | 要素がフォーカスを取得した時に発生する。 バブリングしない。 |
blur |
× | × | 要素がフォーカスを失った時に発生する。 バブリングしない。 |
reset |
○ | ○ | フォームがリセットされた時に発生する。 |
ドキュメント・ウィンドウイベント
| イベント | バブリング | cancelable | 説明 |
|---|---|---|---|
DOMContentLoaded |
○ | × | HTMLの解析が完了し、DOMが構築された時に発生する。 外部リソースの読み込み完了は待たない。 |
load |
× | × | ページ上の全てのリソース (画像、スクリプト等) の読み込みが完了した時に発生する。 |
beforeunload |
× | ○ | ページがアンロードされる直前に発生する。 ユーザへの離脱確認ダイアログを表示できる。 |
resize |
× | × | ウィンドウのサイズが変更された時に発生する。 |
scroll |
× | × | ページまたは要素がスクロールされた時に発生する。 |
ReactのSyntheticEventとの違い
Reactは独自のイベントシステムとして SyntheticEvent を提供している。
下表に、ネイティブDOMイベントとの主な違いを示す。
| 項目 | ネイティブDOM | React (SyntheticEvent) |
|---|---|---|
| 命名規則 | onclickonkeydown (全て小文字) |
onClickonKeyDown (キャメルケース)
|
| ハンドラの値 | 文字列 または 関数参照 | 関数参照のみ |
| イベント委譲 | 手動で親要素に実装する。 | Reactのルート要素に自動委譲される。 |
| イベントオブジェクト | ネイティブ Event オブジェクト |
SyntheticEvent (ネイティブイベントのラッパー)
|
| イベント解除 | removeEventListener を手動で呼び出す。 |
コンポーネントのアンマウント時に自動削除される。 |
| クロスブラウザ対応 | Webブラウザごとに差異がある場合がある。 | 全てのWebブラウザで統一されたAPIを提供する。 |
| ネイティブイベントへのアクセス | そのまま使用 | event.nativeEvent でアクセス可能
|
SyntheticEvent の使用例を以下に示す。
// Reactコンポーネントでのイベント処理
function MyButton() {
const handleClick = (event) => {
// event は SyntheticEvent オブジェクト
console.log(event.type) // "click"
console.log(event.target) // クリックされた要素
// ネイティブイベントへのアクセス
console.log(event.nativeEvent) // ネイティブ Event オブジェクト
}
return <button onClick={handleClick}>クリック</button>
}
Reactでネイティブイベントを直接使用する場合は、useEffect フック内で addEventListener を呼び出す必要がある。
import { useEffect, useRef } from 'react'
function MyComponent() {
const elementRef = useRef(null)
useEffect(() => {
const element = elementRef.current
const controller = new AbortController()
// ネイティブのaddEventListenerを使用する
element.addEventListener('click', handleClick, {
signal: controller.signal
})
// クリーンアップ処理
return () => controller.abort()
}, [])
return <div ref={elementRef}>要素</div>
}
関連情報
- JavaScriptの基礎 - DOM操作
- DOM要素の取得、作成、追加、削除、スタイル操作、closest()メソッドの詳細
- JavaScriptの基礎 - Fetch API
- AbortControllerを使ったリクエストのキャンセル、非同期処理
- JavaScriptの基礎 - エラーハンドリング
- try / catch構文、非同期処理のエラーハンドリング