概要
DOMイベントは、**ユーザー操作に応答するアプリケーションの“神経系”**である。
しかし、安易な addEventListener
の多用や伝播の誤解は、予測不能なバグやメモリリーク、非効率なUI構造を引き起こす。
本稿では以下を体系化する:
- バブリング・キャプチャの仕組みと使い分け
- イベント委譲による効率化
- 明示的なイベント設計のパターン
- リスナー登録・解除の責務と設計
- カスタムイベントの活用と分離設計
1. イベントの伝播:キャプチャとバブリング
✅ 3段階の流れ
1. キャプチャフェーズ(親 → 子)
2. ターゲットフェーズ(対象要素)
3. バブリングフェーズ(子 → 親)
element.addEventListener('click', handler, true); // キャプチャ
element.addEventListener('click', handler, false); // バブリング(デフォルト)
→ ✅ 明示的に true/false
を指定して意図を明文化する
2. バブリング制御と停止
element.addEventListener('click', (e) => {
e.stopPropagation(); // ✅ バブリングを止める
e.preventDefault(); // ✅ デフォルト動作の抑制
});
- ✅
stopPropagation()
→ 他のリスナーに伝播させない - ✅
preventDefault()
→ リンク遷移、フォーム送信などをキャンセル
3. イベント委譲の設計(イベントバインドの最適化)
❌ 各要素にリスナーを直付け
buttons.forEach(btn => btn.addEventListener('click', onClick));
→ 要素が動的に増減する場合、メンテ不能・非効率
✅ 親要素に1つのリスナー(委譲)
document.querySelector('#list').addEventListener('click', (e) => {
if (e.target.matches('.list-item')) {
handleItemClick(e.target.dataset.id);
}
});
- ✅ 子要素にクラスやdata属性を付けて判別
- ✅ 親に1つだけバインド → 高効率・柔軟
4. removeEventListenerの正しい設計
❌ 無名関数では解除不可
element.addEventListener('click', () => console.log('clicked'));
element.removeEventListener('click', () => console.log('clicked')); // ❌ 効かない
→ ✅ 明示的な関数定義が必要
function onClick(e) { ... }
element.addEventListener('click', onClick);
element.removeEventListener('click', onClick);
5. 一度だけ実行するイベント
element.addEventListener('click', handler, { once: true });
→ ✅ once: true
で自動的に1回限りの実行に(手動で解除不要)
6. カスタムイベントとコンポーネント分離
const event = new CustomEvent('user:created', {
detail: { id: 123 }
});
element.dispatchEvent(event);
element.addEventListener('user:created', (e) => {
console.log(e.detail.id); // 123
});
- ✅ UIコンポーネント間の通信に最適
- ✅ “発火”と“受信”を構造的に分離できる
7. イベント命名・スコープ設計指針
設計指針 | 解説 |
---|---|
click , input
|
DOMの基本イベント |
xxx:ready |
状態通知(カスタムイベント) |
user:created |
エンティティ指向のイベント名設計 |
グローバル禁止 | document/window直付けは極力避ける |
コンポーネント内 | 独自要素 or Shadow DOM内で閉じるべき |
設計判断フロー
① 要素が動的に増える? → イベント委譲
② stopPropagation は必要? → 他のハンドラとの関係を設計
③ 一度しか使わないイベント? → once: true を使う
④ イベント解除が必要? → 関数を変数に束縛して管理
⑤ 複数コンポーネント間で通信? → CustomEvent で疎結合化
よくあるミスと対策
❌ 複数バインド → イベントの多重発火
→ ✅ removeEventListener で常に解除戦略を持つ
❌ stopPropagation の乱用
→ ✅ 原因を切るより“構造を見直す”
❌ 同じリスナーが複数バインド
// ループやクリック内でaddEventListenerし続ける
→ ✅ フラグ管理やonceオプションで設計を
結語
イベントとは「ユーザーの意図に対する、アプリケーション側の反応の設計」である。
- DOM構造と伝播構造は一致しない
- 発火点と処理点は分けて考える
- バブリング・キャプチャは“構造上の意味”を持つ
- 再利用されるイベント構造には、明示と設計が必要
“ただ反応する”ではなく、“意図に応じて設計された反応”を構築すること。
それが、モダンなUIの神経設計である。