動的要素にイベントが効かない理由と解決

「追加したボタンだけクリックできない」は正常な挙動

jQueryでよくあるトラブルがあります。
「あとから追加した要素にイベントが効かない」。

例えば次のコードです。

$(".btn").click(function(){
    alert("clicked");
});

$("#add").click(function(){
    $("#area").append('<button class="btn">NEW</button>');
});

既存のボタンは動くのに、NEWボタンは反応しません。
バグに見えますが、これは壊れているわけではありません。

結論から言うと、イベントは「要素」ではなく「その時点の要素」に登録されるためです。

つまり、クリックイベントはセレクタに対して設定されているのではありません。
「その瞬間に存在していた要素」に対して設定されています。

jQueryはセレクタにイベントを付けていない

ここが重要な誤解です。

$(".btn").click(handler);

多くの人は「.btnにイベントを設定した」と思います。
実際には違います。

jQueryは次の処理をしています。

1. 現在のDOMから.btnを検索
2. 見つかった要素一覧を取得
3. それぞれにイベントリスナーを登録

つまり「クラス」ではなく「要素の集合」にイベントを付けています。
そのため後から作られた要素は対象外になります。

DOM生成のタイミングの問題

ブラウザの処理順を簡略化するとこうです。

1. HTML読み込み
2. JavaScript実行
3. clickイベント登録
4. ボタン追加

この順番では、追加されたボタンは登録処理を通っていません。

つまり問題はjQueryではなく「登録タイミング」です。

on()が解決する理由

解決策がこれです。

$(document).on("click", ".btn", function(){
    alert("clicked");
});

これは.btnにイベントを付けていません。
documentに1つだけイベントを置いています。

そしてクリックが起きたとき、
「クリックされた要素が.btnか」を判定しています。

この仕組みをイベントデリゲーション(イベント委譲)と呼びます。

イベントバブリングが鍵

クリックイベントは次の順に伝わります。

button → 親要素 → body → document

これをイベントバブリングと呼びます。
イベントは必ず上に伝播します。

つまりdocumentは、ページ上のすべてのクリックを観測できます。

on()はこの性質を利用し、
「後から追加された要素」も検知できるようにしています。

内部的には何が起きているのか

イメージはこうです。

document.addEventListener("click", function(e){
    if(e.target.matches(".btn")){
        alert("clicked");
    }
});

イベントはdocumentで受け取り、
対象判定だけを後から行っています。

だから、いつ生成された要素でも反応します。

よくある間違い

次の書き方です。

$("#area").append('<button class="btn">NEW</button>');
$(".btn").click(handler);

一見正しそうですが、複数回実行されるとイベントが重複登録されます。
クリック1回で複数回実行される原因になります。

また次も注意です。

$("#area").on("click", function(){
    $(".btn").click(handler);
});

これはさらに危険です。
クリックのたびにイベントが増えます。

documentに付けるべきか

よく「全部documentに付ければいい」と思われますが、それも最適とは限りません。

クリックのたびに判定処理が走るため、
大規模ページでは負荷になることがあります。

理想は「存在が保証されている最も近い親要素」です。

$("#area").on("click", ".btn", handler);

これなら範囲も限定され、パフォーマンスも安定します。

まとめ

動的要素にイベントが効かないのは、jQueryの問題ではありません。
イベントの登録方法の問題です。

click()は「その時点の要素」に結びつきます。
on()は「イベントの流れ」に結びつきます。

DOMは後から変わるものです。
その前提に立つと、イベントは要素に置くのではなく「流れの途中に置く」方が自然です。

この違いを理解すると、なぜフロントエンドの不具合が起きるのか、かなり見通せるようになります。