ブラウザの JavaScript イベントシステム超入門(図解付き)
初心者向けに、図で直感的に理解できるように解説します。
むずかしい用語には(注: …)で短く注釈を入れています。
0. まず「イベント」ってなに?
-
イベントは「出来事」です。
例)ボタンをクリックした、キーを押した、フォームを送信した、ページの読み込みが終わった…など。 - ブラウザは出来事が起きると「イベントが起きたよ!」と知らせます。
JavaScript はその合図を受け取る関数(= イベントリスナー)を登録しておき、呼んでもらいます。
たとえ:インターホン(出来事)→鳴ったら出る(イベントリスナー)
1. どこから来る?だれが受け取る?
イベントはだいたい特定の要素で起きます。
例)<button>
でクリック、<input>
で入力、window
でスクロールや読み込みなど。
イベントが起きた要素を イベントターゲット(注: 発生源の要素)と呼びます。
図1: DOM ツリーのイメージ
ASCII図(Mermaidが使えない場合の代替)
window
└─ document
└─ <div id="box">
└─ <button id="buy">購入</button>
2. どうやって受け取る?(基本の書き方)
最も大事な API は addEventListener
です。
<button id="buy">購入</button>
<script>
const btn = document.getElementById('buy');
// リスナー(= 呼ばれる関数)
function onBuyClick(event) {
console.log('買うボタンが押されました');
}
// 登録
btn.addEventListener('click', onBuyClick);
</script>
- 第1引数:イベント名(例:
'click'
,'input'
,'submit'
など) - 第2引数:呼ばれる関数(イベントリスナー)
- 第3引数:オプション(後述)
補足:
onclick = ...
と書く方法もありますが、1つしか設定できず上書きされやすいので、addEventListener
を基本に使うのがおすすめ。
3. 「イベントオブジェクト」を受け取る
リスナー関数の引数 event
には、出来事の詳細が入っています。
よく使うプロパティ:
-
event.type
… イベント名(例:"click"
) -
event.target
… 実際に起きた要素(注: 内側の要素になることあり) -
event.currentTarget
… いまこの関数が紐づいている要素(安心して使える) -
event.key
… キーボードのキー名(keydown/keyup
で) -
event.clientX / clientY
… クリック位置(ピクセル) -
event.preventDefault()
… ブラウザの既定の動きを止める(後述) -
event.stopPropagation()
… 伝播(注: 親へ広がる動き)を止める(後述)
this
よりevent.currentTarget
を使うと混乱が減ります(特にアロー関数ではthis
が変わるため)。
4. 伝播(でんぱ)ってなに?:キャプチャ → ターゲット → バブリング
イベントは 木構造の内側から外側へ広がる(またはその逆の順で到達する)性質があります。
図2: クリックの伝播フェーズ
キャプチャで受けたいときはオプション { capture: true }
を付けます。
document.addEventListener('click', handler); // バブリング(既定)
document.addEventListener('click', handler, { capture: true }); // キャプチャ
5. 「既定の動き」を止める:preventDefault()
と 伝播停止との違い
図3: preventDefault
と stopPropagation
の違い
-
<a href="#">
クリック → 通常はページ遷移 -
<form>
送信 → 通常はページ遷移/再読み込み -
preventDefault()
:既定の動作だけを止める(伝播は止まらない) -
stopPropagation()
:伝播だけを止める(既定の動作は止まらない)
form.addEventListener('submit', (e) => {
e.preventDefault(); // ページ遷移を止める
// 自分で送信処理(fetch など)を書く
});
さらに強い停止:
event.stopImmediatePropagation()
は 同じ要素に登録された他のリスナーも止めます。
6. addEventListener
の便利オプション
element.addEventListener('event', handler, {
capture: true, // キャプチャ段階で受け取る(既定は false)
once: true, // 1回だけ呼ばれたら自動で解除
passive: true, // このリスナーでは preventDefault しないと約束
signal: abortController.signal // まとめて解除(後述)
});
図4: once
と signal
のイメージ
-
once
:クリック1回だけ反応させたい時に便利。 -
passive
:スクロールやタッチ系イベントで軽くしたい時に有効。
(注:passive: true
だとpreventDefault()
はできません。) -
signal
:AbortController(注: 中断用のコントローラ)で一括解除。
const ac = new AbortController();
window.addEventListener('scroll', onScroll, { signal: ac.signal });
// あとで一気に解除
ac.abort();
7. リスナーの解除:removeEventListener
function onClick(e) { /* ... */ }
button.addEventListener('click', onClick);
// 後で必ず“同じ関数”を渡して解除する
button.removeEventListener('click', onClick);
無名関数(その場のアロー関数)だと解除しにくいので、
後で外す可能性がある処理は名前付き関数にしておくのがコツ。
もしくはonce: true
やAbortController
を使います。
8. イベント委譲(デリゲーション)(注: 親にまとめてつけるテク)
たくさんの子要素に1つずつリスナーを付けるのは重い&管理が大変。
親に1つ付けて、どの子が押されたかを判定するのが「委譲」です。
図5: イベント委譲の動き
<ul id="list">
<li data-id="1">りんご</li>
<li data-id="2">みかん</li>
<li data-id="3">ぶどう</li>
</ul>
<script>
const list = document.getElementById('list');
list.addEventListener('click', (e) => {
const item = e.target.closest('li'); // 近い li を探す
if (!item || !list.contains(item)) return; // 安全チェック
console.log('クリックしたID:', item.dataset.id);
});
</script>
-
利点:あとから
<li>
を追加しても、リスナーを書き足さなくてOK。
9. よく使うイベント一覧(最小実用セット)
目的 | イベント名 | よくある注意点 |
---|---|---|
ボタン押下 | click |
キーボード操作の “Enter/Space でも” クリック扱いに(アクセシビリティ的に嬉しい) |
テキスト入力途中 | input |
1文字ごとに発火。連打対応はデバウンス(注: 連続呼び出しを間引く)推奨 |
入力完了後 | change |
フォーカスが外れた時などに発火。逐次反応には向かない |
フォーム送信 | submit |
e.preventDefault() で自前送信 |
キー入力 |
keydown /keyup
|
e.key を見る('Enter' , 'Escape' など) |
読み込み完了(HTMLだけ) | DOMContentLoaded |
画像などの読み込みは待たない |
完全読み込み(画像含む) |
load (window ) |
すべてのリソースが揃ってから |
スクロール | scroll |
頻繁に起きる → スロットル(注: 一定間隔で処理)推奨 |
フォーカス |
focus / blur
|
バブリングしない。代わりに focusin / focusout はバブリングする |
マウス/タッチ共通 | pointerdown/move/up |
Pointer Events はマウス・タッチ・ペンを統一的に扱える |
10. ちょっと先の理解:イベントループ(超ざっくり)
JavaScript は**基本1本の道(シングルスレッド)**で動きます(注: 同時に2つの JS は実行されないイメージ)。
出来事は「キュー(注: 行列箱)」に並び、空いたときに1つずつ取り出してリスナーを実行します。
これを回す仕組みが イベントループ。
図6: イベントループの全体像(超簡略)
-
重い処理をリスナーに直接書くと、他のイベントが詰まってカクつきます。
→ 小さく分ける、requestAnimationFrame
を使う、デバウンス/スロットルで回数を減らす、などの工夫が必要。
11. ありがちなつまずき
-
preventDefault()
とstopPropagation()
の混同- 前者:既定の動作を止める(リンク遷移・フォーム送信など)。
- 後者:親への伝播を止める(他の場所のリスナーが動かなくなる)。
-
リスナーを重ね付けしてしまう
- 例えば「クリックのたびに
addEventListener
」してしまうと、回数分動く。
→ 1回だけならonce: true
、不要になったらremoveEventListener
。
- 例えば「クリックのたびに
-
無名関数を外せない
- 後で解除する想定があるなら名前付き関数に。
-
this
に頼る- 代わりに
event.currentTarget
を使うと安全。
- 代わりに
-
スクロール・タッチが重い
- 頻度が高いので
passive: true
や スロットルを検討。
- 頻度が高いので
12. すぐ動かせる最小サンプル(フォーム編)
<form id="signup">
<label>
メール:
<input type="email" name="email" required>
</label>
<button>登録</button>
</form>
<script>
const form = document.getElementById('signup');
form.addEventListener('submit', async (e) => {
e.preventDefault(); // 既定の送信を止める
const data = new FormData(form);
const email = data.get('email');
// とりあえずの送信処理(実際は fetch などでサーバへ)
console.log('送信されたメール:', email);
// 成功した想定でボタンの文言を変更
e.currentTarget.querySelector('button').textContent = '送信しました!';
});
</script>
13. イベント委譲の実用サンプル(増えるリスト)
<button id="add">アイテム追加</button>
<ul id="todo"></ul>
<script>
const addBtn = document.getElementById('add');
const list = document.getElementById('todo');
let count = 0;
addBtn.addEventListener('click', () => {
count++;
const li = document.createElement('li');
li.innerHTML = `タスク ${count} <button data-action="done">完了</button>`;
list.appendChild(li);
});
// 親でまとめて受ける
list.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-action="done"]');
if (!btn) return;
const li = btn.closest('li');
li.style.textDecoration = 'line-through';
});
</script>
14. チートシート(迷ったらここ)
-
登録:
el.addEventListener('type', handler, options)
-
解除:
el.removeEventListener('type', handler)
(同じ関数が必要) -
止める
- 既定の動作:
event.preventDefault()
- 伝播:
event.stopPropagation()
/ (さらに強い)event.stopImmediatePropagation()
- 既定の動作:
-
どの要素?
- 発生源:
event.target
- いま処理中の要素:
event.currentTarget
- 発生源:
-
使い分け
- 逐次反応:
input
- 入力完了:
change
- 1回だけ:
{ once: true }
- スクロール/タッチ軽量化:
{ passive: true }
(preventDefault
は使えない) - まとめて外す:
AbortController
のsignal
- 逐次反応:
用語ミニ辞典
- DOM(注: Document Object Model)… HTML を木構造として扱う仕組み。要素を JS で触れるようにするもの。
- バブリング… イベントが内側 → 外側へ広がること。
- キャプチャ… イベントが外側 → 内側へ下りてくる段階。
- デバウンス… 連続で起きるイベントを最後の1回にまとめるテクニック。
- スロットル… 多発するイベントを一定間隔ごとに処理するテクニック。
- イベントループ… たまった出来事を1つずつ処理する仕組み。
付記:Qiitaでの図表示について
- 本記事の図は Mermaid を使用しています。Qiita は Mermaid に対応しています。
- もし組織設定などで Mermaid が無効な場合は、各図の ASCII版 を参照してください。