Step 1: Web Basics
04. イベントリスナー入門:登録・解除とボタンが動かない原因
イベントは、ファイル選択やクリックなどの操作を処理へつなぐ入口です。登録した関数がいつ動くのか、複数登録した場合に何が起こるのかを分けて考えます。猫の手ツールのメニューは、再設定時に前のリスナーを解除して、処理が重複しないようにしています。
1. HTMLにJavaScriptを書くのは「密結合」のリスク
Webサイトに動きをつけようとしたとき、真っ先に思いつくのがHTML要素の属性として直接JavaScriptを記述する onclick 属性ではないでしょうか。
<!-- 避けるべき古い書き方:インラインイベントハンドラ -->
<button onclick="alert('クリックされました!')">送信</button> この方法は手軽ですが、プロジェクトが大きくなるにつれて「どこにどんなロジックが書かれているか」が分からなくなる「密結合」という状態を招きます。インフラの世界で例えるなら、サーバーの起動スクリプトの中にOSの設定を直接書き込んでしまうようなものです。設定(HTML)と実行ロジック(JavaScript)は、明確に分離すべきなのです。
2. 監視役を任命する:addEventListener
そこで登場するのが addEventListener です。これは要素に対して「特定のイベント(クリックやスクロールなど)が起きたら、この関数を実行してね」という監視役を任命する仕組みです。
HTML側には id だけを振り、JavaScript側ですべての挙動をコントロールすることで、画面の構造と操作の処理を分けて読みやすくなります。
コードで見る Before / After
【Before】HTMLの中にロジックが漏れ出している状態
<button onclick="processData()" id="save-btn">保存</button> 【After】HTMLは構造に専念し、JSで挙動を定義する状態
// HTMLは <button id="save-btn">保存</button> のみ
const saveBtn = document.getElementById('save-btn');
// 推奨される現代的な記述
if (saveBtn) {
saveBtn.addEventListener('click', () => {
console.log('保存ボタンが押されました');
// 実際の保存処理はここから呼び出す
});
} 3. なぜ addEventListener を使うべきなのか
単に「綺麗だから」という理由だけではありません。addEventListener には、属性ベースの onclick にはない強力なメリットがあります。
- 複数の処理を登録できる:
onclickは1つの要素に1つしか登録できませんが、リスナーなら「ログを記録する」「色を変える」などの処理を複数登録できます。リスナーを増やすだけで処理が並列になるわけではありません。 - 動的な解除が可能:
removeEventListenerを使えば、特定の条件下で監視を止めることができます。これはメモリ管理の面でも非常に重要です。 - 登録オプションを指定できる: capture、once、passive などを選べます。stopPropagation() による伝播停止は onclick のハンドラーでも使えるため、addEventListener 専用の機能ではありません。
4. 注意点:初心者がやりがちな登録する関数に「()」を付けてしまうこと
関数の参照と、関数を実行した結果を取り違えると、クリック前に処理が動いてしまいます。
// ❌ 間違い:関数がその場で実行されてしまう
btn.addEventListener('click', doSomething());
// ✅ 正解:関数の「名前(参照)」だけを渡す
btn.addEventListener('click', doSomething); () を付けると、ボタンが押される前にスクリプトが読み込まれた瞬間に実行されてしまいます。イベントリスナーには「いつか実行するためのレシピ」を渡すのであって、「今すぐの結果」を渡すのではないという感覚が大切です。
5. 実務での活用:猫の手ツールの場合
「猫の手ツール」の開発において、イベントリスナーは至る所で活躍しています。
- 画像選択:
<input>要素のchangeイベントを監視して、写真が選ばれた瞬間にプレビューを生成する。 - ドラッグ&ドロップ:
dragoverで preventDefault() を呼んでドロップを受け付け、dropでも既定の動作を止めてファイルを受け取る。処理対象の要素やファイル種類を確認してから画像加工へ渡す。 - ダウンロードボタン:
clickイベントを契機にCanvasの描画結果を書き出し、保存用のリンクや画像を表示する。書き出し形式と保存手順はツールごとに異なる。
イベントの入口でファイルや操作状態を確認し、画像処理は別の関数へ渡すと、画面操作と加工の処理を追いやすくなります。
同じ画面で登録を繰り返す場合の解除
関数を呼ぶたびに匿名関数を登録すると、1回のクリックで処理が何度も動く原因になります。解除には登録時と同じ関数参照を使います。以下は単独で試せる例です。
const button = document.createElement('button');
button.textContent = '確認';
document.body.append(button);
function handleClick() { console.log('1回クリック'); }
button.addEventListener('click', handleClick);
// 不要になった時点で解除する
// button.removeEventListener('click', handleClick); メニューを再設定するとき、前のリスナーをまとめて解除する
このサイトの共通メニューでは、setupSidebar()の先頭で前のAbortControllerをabortし、新しいsignalでキーボード等のリスナーを登録します。処理の再設定と、匿名関数を追加し続ける処理を区別した実装です。
sidebarEvents?.abort();
sidebarEvents = new AbortController();
const { signal } = sidebarEvents;
desktop.addEventListener('change', () => setOpen(false, false), { signal });
// キーボードのリスナーも同じsignalで登録する。抜粋のdesktopはメディアクエリ、setOpen()は開閉処理です。画像変換ツールのドラッグ&ドロップにもaddEventListenerを使います。一方、本番にはelement.onclickやHTMLのonclickも残るため、すべて同じ方式に移行済みとは言えません。登録を増やすか置き換えるか、解除の単位をどうするかで選びます。画像変換の制作解説へ進む前に、以下の小さい例で1クリック1回を確認してください。
まとめ
一度だけ処理するなら { once: true } も使えます。外部スクリプトで既存の要素を取得する例は、対象HTMLを読み込んだ後(type="module"、defer、またはDOM構築後)に実行してください。