投稿日:2026/8/6
更新日:2026/8/6

addEventListener は「このできごとが起きたら、この関数を呼んで」をブラウザに登録するメソッド。
引数は 3 つしかないのに、第 3 引数まわり(once / passive / capture / signal)で毎回迷うので整理しておく。
target.addEventListener(type, listener, options) の 3 つ。type は on を付けない文字列('click')options)を渡すのが今の書き方。旧来の true / false は capture だけを指定した形removeEventListener で消せるのは「同じ関数参照 + 同じ capture」のときだけ。無名関数は消せないので once か signal を使うpassive: true は「preventDefault() しません」というブラウザへの約束。スクロールが軽くなる代わりに preventDefault() は無視されるtarget.addEventListener(type, listener, options);
// 旧来の書き方(第3引数が真偽値)
target.addEventListener(type, listener, useCapture);
const button = document.querySelector('#submit');
button.addEventListener('click', (event) => {
console.log('クリックされました', event.target);
});
| 引数 | 必須 | 型 | 中身 |
|---|---|---|---|
type |
✅ | 文字列 | イベント名。'click' / 'keydown' / 'submit' |
listener |
✅ | 関数 または オブジェクト | 発火時に呼ばれる処理 |
options |
— | オブジェクト または 真偽値 | 発火タイミングと後片付けの制御 |
監視したいイベントの種類を文字列で指定する。onclick のような on は付けない。
| 分類 | 例 |
|---|---|
| ユーザー操作 | click / keydown / input / submit / mousemove |
| 画面・ブラウザの変化 | scroll / resize / DOMContentLoaded / visibilitychange |
| メディア・通信 | play / ended / online / offline |
| 独自イベント | CustomEvent で自作した任意の名前(後述) |
タイプミスしてもエラーにならない。'clik' と書いても登録は成功し、ただ永遠に発火しないだけなので、動かないときはまずここを疑う。
イベントオブジェクトが第1引数として自動で渡される。
form.addEventListener('submit', (event) => {
event.preventDefault(); // デフォルト動作(ページ遷移)を止める
console.log(event.type); // 'submit'
console.log(event.target); // 実際にイベントが起きた要素
console.log(event.currentTarget); // リスナーを登録した要素(= form)
});
targetとcurrentTargetは別物。子要素をクリックしたときtargetは子、currentTargetは登録した親になる。
イベント委譲(親でまとめて受けてevent.targetで判定する)はこの差を使っている。
handleEvent を持つオブジェクトを渡す関数以外に、handleEvent メソッドを持つオブジェクトも渡せる。状態を持たせたいときに使う。
const counter = {
count: 0,
handleEvent(event) {
this.count += 1; // this はこのオブジェクト
console.log(`${this.count} 回目`);
},
};
button.addEventListener('click', counter);
this が何を指すかは渡し方で変わる。
| 渡し方 | リスナー内の this |
|---|---|
function 式 |
event.currentTarget(登録した要素) |
| アロー関数 | 定義した場所の this(要素ではない) |
handleEvent を持つオブジェクト |
そのオブジェクト自身 |
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
once |
Boolean | false |
true で 1 度実行された後に自動でリスナーが外れる |
passive |
Boolean | false(※例外あり) |
true で「preventDefault() しない」と宣言し、スクロール性能を上げる |
capture |
Boolean | false |
true でキャプチャフェーズ(親 → 子)で実行する |
signal |
AbortSignal | — | AbortController の signal。外から一括で解除できる |
第 3 引数に真偽値を直接渡す旧来の書き方。capture だけを指定した形と等価。
el.addEventListener('click', fn, true);
el.addEventListener('click', fn, { capture: true }); // 同じ意味
イベントは window から降りてきて(キャプチャ)、ターゲットに到達し、また上へ戻る(バブリング)。capture はこのどちらで拾うかの指定。
flowchart TD
W1["window"] --> D1["document"]
D1 --> P1["div#parent"]
P1 --> T["button#child<br/>(ターゲット)"]
T --> P2["div#parent"]
P2 --> D2["document"]
D2 --> W2["window"]
C["キャプチャフェーズ<br/>capture: true"] -.- W1
B["バブリングフェーズ<br/>capture: false(デフォルト)"] -.- W2
parent.addEventListener('click', () => console.log('親:キャプチャ'), { capture: true });
parent.addEventListener('click', () => console.log('親:バブリング'));
child.addEventListener('click', () => console.log('子'));
// child をクリックしたときの出力
// 親:キャプチャ → 子 → 親:バブリング
ターゲット自身に登録したリスナーは、
captureの値によらず登録した順に呼ばれる(ターゲットフェーズ)。
順序が効いてくるのは祖先要素に登録したリスナーだけ。
途中で止めたいときは event.stopPropagation()(以降の伝播を止める)。同じ要素に付いた他のリスナーまで止めるなら event.stopImmediatePropagation()。
| 方法 | 向いている場面 | 注意 |
|---|---|---|
removeEventListener |
任意のタイミングで個別に外す | 同じ関数参照と同じ capture が必要 |
{ once: true } |
1 回きりの処理(初回クリック、読み込み完了) | 発火しなければ残り続ける |
{ signal } |
複数のリスナーをまとめて外す(SPA の後片付け) | AbortController を保持しておく必要がある |
// ① removeEventListener:関数を変数に入れておく
const onScroll = () => console.log('スクロール中');
window.addEventListener('scroll', onScroll);
window.removeEventListener('scroll', onScroll);
// ② once:1度実行されたら自動で外れる
button.addEventListener('click', () => {
console.log('最初の一回だけ実行されます');
}, { once: true });
// ③ signal:まとめて解除
const controller = new AbortController();
const { signal } = controller;
window.addEventListener('resize', onResize, { signal });
window.addEventListener('scroll', onScroll, { signal });
document.addEventListener('keydown', onKeydown, { signal });
controller.abort(); // 上の3つが一度に外れる
signal の後片付けはフレームワークのクリーンアップと相性がよい。
// React の useEffect での例
useEffect(() => {
const controller = new AbortController();
window.addEventListener('resize', handleResize, { signal: controller.signal });
window.addEventListener('orientationchange', handleResize, { signal: controller.signal });
return () => controller.abort(); // アンマウント時にまとめて解除
}, []);
removeEventListener で外せないel.addEventListener('click', () => console.log('hi'));
el.removeEventListener('click', () => console.log('hi')); // 外れない
見た目が同じでも別の関数オブジェクトなので一致しない。.bind(this) や () => fn() でラップした場合も同様に毎回新しい関数になる。変数に入れておくか、once / signal を使う。
capture が一致しないと外れないel.addEventListener('click', fn, { capture: true });
el.removeEventListener('click', fn); // 外れない(capture が false 扱い)
el.removeEventListener('click', fn, { capture: true }); // 外れる
照合に使われるのは type / 関数参照 / capture の 3 つだけ。once や passive は照合に関係しない。
passive のデフォルトは常に false ではないwindow / document / document.body に登録した touchstart touchmove wheel などのスクロール系イベントは、明示しなければ passive: true として扱われる(スクロールのカクつきを防ぐためのブラウザ側の仕様)。
// これは preventDefault() が効かず、コンソールに警告が出る
window.addEventListener('touchmove', (e) => e.preventDefault());
// スクロールを止めたいなら明示する
window.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false });
逆に、スクロール中に重い処理をしていないのに描画が重いときは
{ passive: true }を明示すると改善することがある。
type / 関数参照 / capture がすべて同じ登録は、2 回目以降が捨てられる。
el.addEventListener('click', fn);
el.addEventListener('click', fn); // 無視される → 1回しか呼ばれない
el.addEventListener('click', fn, { capture: true }); // capture が違うので別枠で登録される
「重複登録で 2 回発火した」ときは、たいてい毎回違う無名関数を登録している(コンポーネントの再描画のたびに登録するなど)。
focus / blur / スクロール要素の scroll などは親に伝播しない。イベント委譲で拾えないので、バブリングする代替イベント(focusin / focusout)を使う。
DOMContentLoaded は登録が遅いと二度と来ない<script> の実行タイミングによっては、登録した時点で既に発火済みのことがある。その場合は document.readyState で判定する。
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
ブラウザ標準のイベントだけでなく、CustomEvent で自作したイベントも同じ仕組みで受け取れる。任意のデータは detail に載せる。
// 待ち受ける
document.addEventListener('cartUpdated', (event) => {
console.log('カートが更新されました:', event.detail); // { itemId: 123, quantity: 2 }
});
// 好きなタイミングで発火させる
document.dispatchEvent(new CustomEvent('cartUpdated', {
detail: { itemId: 123, quantity: 2 },
bubbles: true, // 親へ伝播させたいなら明示(デフォルトは false)
}));
sequenceDiagram
participant Code as 発火側のコード
participant Target as EventTarget<br/>(document / 要素)
participant Listener as 登録済みリスナー
Listener->>Target: addEventListener('cartUpdated', fn)
Note over Target: リスナーを保持
Code->>Target: dispatchEvent(new CustomEvent(...))
Target->>Listener: fn(event) を実行
Note over Listener: event.detail でデータを受け取る
標準イベントでも自作イベントでも、「特定のできごとが起きたら、登録しておいた関数を実行する」という役割は変わらない。
'use client' 側に置く必要がある話