JavaScript実践 #2 イベントハンドリングと委譲
#1 DOM操作の基本 でエレメントを扱うツールを見ました。今回はそれらでユーザーインタラクションを処理する場面です。
イベント登録 — addEventListener
#
const btn = document.querySelector('#submit');
btn.addEventListener('click', (e) => {
console.log('クリックされた');
});addEventListener('タイプ', ハンドラ) — これがモダンなJavaScriptの標準です。
古いコードには btn.onclick = () => {...} のように直接代入する方式も見かけますが、一つのエレメントに一つのハンドラだけ登録できる制限があります。addEventListener は同じイベントに複数のハンドラを登録できます。
よく使うイベントタイプ #
click マウスクリック
input 入力フィールドの値変更 (リアルタイム)
change 値変更 (フォーカスを失う時)
submit フォーム送信
keydown/keyup キー入力
focus/blur フォーカス取得/喪失
mouseenter マウス進入
mouseleave マウス離脱
scroll スクロール
load ロード完了
DOMContentLoaded HTML パース完了イベントオブジェクト — e
#
ハンドラが受け取る引数はイベントオブジェクト。よく使うプロパティ。
btn.addEventListener('click', (e) => {
e.target; // イベントが発生したエレメント
e.currentTarget; // ハンドラが登録されたエレメント
e.type; // 'click'
e.preventDefault(); // デフォルト動作を防ぐ
e.stopPropagation(); // バブリングを止める
});e.target vs e.currentTarget
#
最も混乱する場面です。
<button class="card">
<span>クリックしてください</span>
</button>btn.addEventListener('click', (e) => {
console.log(e.target); // <span> (実際にクリックされたエレメント)
console.log(e.currentTarget); // <button> (ハンドラ登録エレメント)
});target— イベントが発生したエレメント (最も内側のクリック対象)currentTarget— ハンドラが登録されたエレメント
イベント委譲パターンで二つの違いが重要です。少し後で見ます。
イベントの流れ — キャプチャリングとバブリング #
イベントが発生すると二段階で伝播します。
1. キャプチャリング (capturing) — document → イベント発生位置まで 上→下
2. ターゲット (target) — 発生位置に到達
3. バブリング (bubbling) — 発生位置 → document まで 下→上デフォルトではバブリング段階でハンドラが実行されます。
<div id="outer">
<div id="inner">
<button id="btn">クリック</button>
</div>
</div>document.querySelector('#outer').addEventListener('click', () => console.log('outer'));
document.querySelector('#inner').addEventListener('click', () => console.log('inner'));
document.querySelector('#btn').addEventListener('click', () => console.log('button'));
// ボタンクリック時の出力:
// button
// inner
// outer
イベントがボタン → inner → outer へと上に流れていき、それぞれのハンドラが順番に実行されます。これがバブリングです。
キャプチャリングで聴く #
document.querySelector('#outer').addEventListener(
'click',
() => console.log('outer (キャプチャ)'),
{ capture: true }
);3番目の引数に { capture: true } を渡すとキャプチャ段階でハンドラが実行されます。ほぼ使いませんが、「上側が先に処理しなければならない場面」があるときにたまに役立ちます。
stopPropagation と preventDefault
#
stopPropagation — 伝播を止める
#
button.addEventListener('click', (e) => {
e.stopPropagation(); // これ以上上に上がらない
});上のハンドラで stopPropagation を呼ぶと — outer/innerハンドラは呼び出されません。
preventDefault — デフォルト動作を防ぐ
#
form.addEventListener('submit', (e) => {
e.preventDefault(); // フォームがページをリロードしない
// ... 直接処理
});
link.addEventListener('click', (e) => {
e.preventDefault(); // リンクが遷移しない
});ブラウザのデフォルト動作を防ぎます。フォーム送信、リンクの遷移、右クリックメニューなどでよく出会います。
二つのメソッドは目的が違います。stopPropagation はJavaScriptハンドラの伝播を、preventDefault はブラウザのデフォルト動作を、それぞれ制御します。混同しないように別々に置きましょう。
イベント委譲 — 効率的なパターン #
リスト内に100個の項目があり、それぞれにクリックハンドラが必要なら。
良くないパターン — 項目ごとにハンドラ #
document.querySelectorAll('.item').forEach((item) => {
item.addEventListener('click', (e) => {
console.log('クリック:', item.dataset.id);
});
});100個のハンドラがメモリに登録されます。リストに動的に項目が追加されると、新しい項目にはハンドラがありません。
良いパターン — 親に一つのハンドラ #
const list = document.querySelector('.list');
list.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (!item || !list.contains(item)) return;
console.log('クリック:', item.dataset.id);
});ハンドラは一つだけ。クリックが起きるとバブリングで親まで上がってきて、そこで closest でどの項目がクリックされたかを確認します。
委譲の利点 #
- メモリ節約 — ハンドラが一つ
- 動的項目の自動処理 — 後から追加された項目も同じハンドラで動作
- 簡潔さ — リストが変わるたびにハンドラを登録/解除しなくて済む
リスト、テーブル、カードグリッドのような場面でほぼ標準パターンです。
ハンドラの削除 — removeEventListener
#
function onClick() {
console.log('クリック');
}
btn.addEventListener('click', onClick);
// 後で
btn.removeEventListener('click', onClick);ハンドラ関数の参照が同じでなければ削除できません。アロー関数を即席で登録すると削除できません。
btn.addEventListener('click', () => console.log('クリック'));
btn.removeEventListener('click', () => console.log('クリック')); // ✗ 別の関数
AbortController — モダンなハンドラ片付け
#
中級 #6 fetch API で見たAbortControllerがイベントの片付けにも使えます。
const controller = new AbortController();
btn.addEventListener('click', onClick, { signal: controller.signal });
input.addEventListener('input', onInput, { signal: controller.signal });
window.addEventListener('resize', onResize, { signal: controller.signal });
// 一度にすべて削除
controller.abort();複数のハンドラを一度に片付けられます。コンポーネントのcleanupの場面に非常に向いています。
一度だけ実行 — { once: true }
#
btn.addEventListener('click', () => {
console.log('最初で最後');
}, { once: true });自分自身を自動で削除します。一回限りのハンドラに非常にすっきりします。
Passive イベント — スクロールパフォーマンス #
scroll、touchmove のようなイベントは passive で登録するのが推奨されます。
window.addEventListener('scroll', onScroll, { passive: true });passiveハンドラは preventDefault を呼ばないという約束です。その約束があれば、ブラウザはスクロールをより速く処理できます。
フォーム/入力でよく使うパターン #
input vs change
#
input.addEventListener('input', (e) => {
// 各キー入力ごとに
console.log(e.target.value);
});
input.addEventListener('change', (e) => {
// 入力が終わってフォーカスを失う時 (または select 変更時)
console.log(e.target.value);
});検索のオートコンプリートのようなリアルタイム処理は input。検証/保存のような重い作業は change またはデバウンスされた input。
キーボードイベント #
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') submit();
if (e.key === 'Escape') close();
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
save();
}
});e.key がモダンな標準。古い e.keyCode はdeprecatedです。
カスタムイベント #
自分だけのイベントを作ってdispatchできます。
const event = new CustomEvent('user-login', {
detail: { id: 'u1', name: 'カーティス' },
});
document.dispatchEvent(event);
// 別の場所で
document.addEventListener('user-login', (e) => {
console.log(e.detail); // { id: 'u1', name: 'カーティス' }
});大きなアプリでモジュール間の通信にたまに活用します。ただし使いすぎると流れの追跡が難しくなるので慎重に。
まとめ #
今回の記事で整理した内容。
addEventListenerがモダンな標準e.target(実際の発生) vse.currentTarget(ハンドラ登録位置)- イベントの流れ: キャプチャリング → ターゲット → バブリング (デフォルトはバブリング)
stopPropagationは伝播、preventDefaultはデフォルト動作- イベント委譲 — 親に一度だけ登録、
closestで位置確認 removeEventListenerは同じ関数参照が必要AbortControllerで複数のハンドラを一度に片付け{ once: true }、{ passive: true }オプションinputはリアルタイム、changeは入力完了時点e.keyがキーボードイベントのモダンな標準
次回の記事 (#3 fetch と非同期 UI) では、fetchでデータを受け取ってDOMに反映するパターン — ローディング/エラー状態の表示、デバウンス、AbortControllerまで束ねて扱います。