JavaScriptの基礎 - イベント(DOM)

提供: MochiuWiki : SUSE, EC, PCB

概要

DOMイベントとは、Webブラウザ上で発生するユーザ操作やシステム上の出来事を検出して処理するための仕組みである。
クリック、キー入力、フォーム送信、ページ読み込み等の出来事がイベントとして発生し、登録されたリスナー関数が呼び出される。

イベントは EventTarget インターフェースを実装する全てのオブジェクトで発生する。
代表的な実装対象として、ElementDocumentWindow があり、XMLHttpRequestAudioNode 等もイベントを送出する。

イベントの登録には、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)


下表に、引数の説明を示す。

addEventListener の引数一覧
引数 説明
登録するイベントの種類を表す文字列
例: 'click', 'keydown', 'submit'
listener イベント発生時に呼び出されるコールバック関数
引数としてイベントオブジェクトを受け取る。
options オプションオブジェクト または useCapture の真偽値 (省略可能)


使用例を以下に示す。

 const button = document.getElementById('myButton')
 
 button.addEventListener('click', (event) => {
    console.log('クリックされました')
    console.log(event.target)
 })


オプション

第3引数にオブジェクトを渡すことにより、リスナーの動作を細かく制御できる。

addEventListenerオプション一覧
オプション デフォルト 説明
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 オプション

AbortControllerAbortSignal を組み合わせることにより、複数のリスナーを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


下表に、targetcurrentTarget の違いを示す。

target と currentTarget の違い
プロパティ 説明
event.target イベントが最初に発火した実際の要素
子要素をクリックした場合はその子要素を指す。
event.currentTarget リスナーが登録された要素
バブリング中は親要素を指すこともある。


共通メソッド

全てのイベントオブジェクトが持つ主要なメソッドを以下に示す。

preventDefault

cancelabletrue のイベントに対して、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 イベント発生時に対応する修飾キーが押されていたかどうか


キーボードイベントのプロパティ

キーボードイベント固有の重要なプロパティを以下に示す。

key と code の違い
プロパティ 説明 例: 日本語キーボードで[Y]キーを押下した場合
key ユーザのキーボードレイアウトに依存した実際の文字 (論理キー) "y" または "Y"
code キーボードレイアウトに依存しない物理的なキーの位置 (物理キー) "KeyY" (常に固定)


下表に、使い分けの指針を示す。

key と code の使い分けの指針
プロパティ 使用する場面
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つのフェーズを順番に経る。

  1. キャプチャリング段階 (eventPhase: 1)
    イベントはウィンドウから発生した要素 (ターゲット) に向かって伝播する。
    capture: true で登録したリスナーがこのフェーズで実行される。

  2. ターゲット段階 (eventPhase: 2)
    イベントがターゲット要素に到達した状態。
    ターゲット要素に登録されたリスナーが実行される。

  3. バブリング段階 (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 vs React のイベント比較
項目 ネイティブDOM React (SyntheticEvent)
命名規則 onclick
onkeydown (全て小文字)
onClick
onKeyDown (キャメルケース)
ハンドラの値 文字列 または 関数参照 関数参照のみ
イベント委譲 手動で親要素に実装する。 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>
 }



関連情報