2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザの JavaScript イベントシステム超入門(図解付き)

Posted at

ブラウザの 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: preventDefaultstopPropagation の違い

  • <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: oncesignal のイメージ

  • once:クリック1回だけ反応させたい時に便利。
  • passive:スクロールやタッチ系イベントで軽くしたい時に有効。
    (注: passive: true だと preventDefault() はできません。)
  • signalAbortController(注: 中断用のコントローラ)で一括解除。
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: trueAbortController を使います。


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 画像などの読み込みは待たない
完全読み込み(画像含む) loadwindow すべてのリソースが揃ってから
スクロール scroll 頻繁に起きる → スロットル(注: 一定間隔で処理)推奨
フォーカス focus / blur バブリングしない。代わりに focusin / focusout はバブリングする
マウス/タッチ共通 pointerdown/move/up Pointer Events はマウス・タッチ・ペンを統一的に扱える

10. ちょっと先の理解:イベントループ(超ざっくり)

JavaScript は**基本1本の道(シングルスレッド)**で動きます(注: 同時に2つの JS は実行されないイメージ)。
出来事は「キュー(注: 行列箱)」に並び、空いたときに1つずつ取り出してリスナーを実行します。
これを回す仕組みが イベントループ

図6: イベントループの全体像(超簡略)

  • 重い処理をリスナーに直接書くと、他のイベントが詰まってカクつきます。
    → 小さく分ける、requestAnimationFrame を使う、デバウンス/スロットルで回数を減らす、などの工夫が必要。

11. ありがちなつまずき

  1. preventDefault()stopPropagation() の混同
    • 前者:既定の動作を止める(リンク遷移・フォーム送信など)。
    • 後者:親への伝播を止める(他の場所のリスナーが動かなくなる)。
  2. リスナーを重ね付けしてしまう
    • 例えば「クリックのたびに addEventListener」してしまうと、回数分動く。
      → 1回だけなら once: true、不要になったら removeEventListener
  3. 無名関数を外せない
    • 後で解除する想定があるなら名前付き関数に。
  4. this に頼る
    • 代わりに event.currentTarget を使うと安全。
  5. スクロール・タッチが重い
    • 頻度が高いので 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 は使えない)
    • まとめて外す:AbortControllersignal

用語ミニ辞典

  • DOM(注: Document Object Model)… HTML を木構造として扱う仕組み。要素を JS で触れるようにするもの。
  • バブリング… イベントが内側 → 外側へ広がること。
  • キャプチャ… イベントが外側 → 内側へ下りてくる段階。
  • デバウンス… 連続で起きるイベントを最後の1回にまとめるテクニック。
  • スロットル… 多発するイベントを一定間隔ごとに処理するテクニック。
  • イベントループ… たまった出来事を1つずつ処理する仕組み。

付記:Qiitaでの図表示について

  • 本記事の図は Mermaid を使用しています。Qiita は Mermaid に対応しています。
  • もし組織設定などで Mermaid が無効な場合は、各図の ASCII版 を参照してください。
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?