実践
第1回 .on()と.off()を使いこなす 1

jQuery 1.7から実装された.on()と.off()メソッドを取り上げます。jQueryオブジェクトに対してイベントの定義を行う機能をきちんと理解して、イベントを自在にコントロールできるようになりましょう。

2014年06月12日発行

目次

この『実践、jQuery』シリーズは、jQueryを使った実装がある程度できるようになったエンジニアが、さらにjQueryへの理解を深めていくためのシリーズです。

今回は、jQuery 1.7*から実装された.on().off()という2つのメソッドを取り上げます。これらのメソッドが担う、jQueryオブジェクトに対してイベントの定義を行うための機能をきちんと理解することで、イベントを自在にコントロールできるようになることを目的としています。

*注:jQuery 1.7

jQuery 1.7は2011年11月にリリースされたバージョンです。jQuery 1.xは1.11.1が最新。jQuery 2.xは2.1.1が最新バージョンです。系統がわかれた経緯については「jQuery Conf. 2012 レポート:Key Note・全体の感想」などを参照してください。

.on()を用いたイベントの定義

jQueryオブジェクトにイベントを定義するとき、普段どのようにコードを書いていますか?

jQueryの勉強を始めた頃のことを思い出してみてください。例えば「ボタンをクリックしたらアラートを出す」といった実装をする場合、入門書にはこのように書かれていたのではないでしょうか。

$('button').click(function() {
  alert('クリックされました');
});

もちろんこの記述で正しく動作します。しかしjQueryでは、次のように.on()を使ってイベントを定義することもできます。

$('button').on('click', function() {
  alert('クリックされました');
});

実は、さきほどの.click(fn).on('click', fn)へのショートカットメソッドであり、jQueryの内部で.on()が呼び出されることで.click()が動作しています。

一見すると、.click()を使った方が直感的でわかりやすそうですが、実は.on()には次節から紹介するいろいろな機能があります。

.on()でできること

では、.on()の機能の概要を紹介します。いくつかの機能に関しては、今回と次回の記事で詳しく解説します。

一度に複数のイベントタイプを定義できる

複数のイベントタイプを半角スペースで区切って指定することで、複数のイベントに対して一括して、同じイベントハンドラ(イベントが起こったときに実行される関数)を定義することができます。

例えば、入力フォームにテキストを入力するたびにバリデーションチェックを行いたい場合、まずは、キーボードからの入力に対応するために、キーを離したときに発生するkeyupに対してイベントハンドラを紐付けます。しかし、それだけでは、コンテキストメニューからペーストされた場合には対応できません。

次のようにpasteイベントを併せて指定することで、両方に対応することができます。

$('input').on('keyup paste', function() {
  // バリデーションの処理
});

複数のイベントとイベントハンドラを一括定義できる

.on()の第一引数にイベントタイプをキー、イベントハンドラを値としたオブジェクトを指定することで、イベントごとに異なる処理をする、複数のイベントハンドラを一括して定義できます。

例えば、.hover()と同じ挙動を.on()で実装するには、次のように記述します。

$('img').on({
  'mouseenter': function() {
    // マウスオーバー時の処理
  },
  'mouseleave': function() {
    // マウスアウト時の処理
  }
});

データを渡すことができる

.on()では、次のように引数を渡します([]内は省略可)。

.on( events [, selector ] [, data ], handler(eventObject) );

dataにキーと値の組み合わせのデータを渡しておくと、そのイベントが発火したときにイベントオブジェクトを通してevent.dataにその値が格納され、イベントハンドラから扱えるようになります。

これにより、共通のイベントハンドラを使った異なる処理を実現できます。

次の例では、イベント定義時に『自分が何番目のボタンか』という値をイベントハンドラに渡しているため、ボタンごとに表示されるアラートの内容を変えられます。

.on()による値の受け渡し

ソースコードは次のようになっています。

<button>1</button>
<button>2</button>
<button>3</button>
var i;
 
for (i = 0; i < 3; i++) {
  $('button').eq(i).on('click', { value: i + 1 }, onClick);
}
 
function onClick(e) {
  alert(e.data.value + '番目のボタンをクリックしました');
}

イベントの名前空間を指定できる

.on()で定義したイベントは、.off()を使って解除できます。

次の例では、buttonに対して.on()で設定されたclickイベントを、.off()で解除しています。

// クリックイベントの定義
$('button').on('click', function() {
  alert('クリックされました');
});
 
// クリックイベントの定義を解除
$('button').off('click');

しかし、もし別の場所でbuttonに対して、別のclickイベントが定義されていた場合、前述の書き方では、それも一緒に解除されてしまうため、意図せぬバグが発生してしまう可能性があります。

このような問題を解決するために、イベントに名前空間を設定することで、.off()の影響範囲をコントロールできる機能があります。

イベントの名前空間については、次回、詳しく解説します。

あとから追加する要素にもイベントを定義できる

イベントの発生する要素に直接イベントを定義する場合、あとからHTMLに追加した要素に対しては、イベントハンドラが実行されません。

このような場合、.on()を使って祖先要素を起点としてイベントを定義することで、あとから追加した要素でもイベントハンドラが実行されるようになります。

詳しくは、次の節で解説します。

デリゲートを利用したイベント設定

例えば、次のようにli要素がクリックされたとき、その文字色を赤にしたいとします。

$('li').on('click', function() {
  $(this).css('color', 'red');
});

すると当然、li要素がクリックされれば、その文字色は赤になります。

しかし、次のような場合はどうでしょう。デモ上のボタンをクリックして新しいli要素を増やし、増えたli要素をクリックしてみてください。

あとから追加した要素にはイベントハンドラが実行されない
はじめからあるli要素は、クリックすると文字色が赤く変わるが、ボタンであとから追加されたli要素は、クリックしても文字色は変わらない。

新しく追加されたli要素は赤くなりません。なぜこのようなことが起こってしまうかというのは、順を追って考えればわかります。前掲したコードは、ページ上にあるli要素がクリックされたとき、自身の文字色を赤にするというイベント定義を行うコードです。

$('li').on('click', function() {
  $(this).css('color', 'red');
});

ここで$('li')で取得している要素というのは、その時点でページ上に存在している要素なのです。それらの要素にクリックイベントを定義しているわけですから、当然、はじめからページに存在しているli要素ではイベントハンドラが実行されます。しかし、それ以降に追加された要素というのは、このコードが実行された時点では、どこにも存在していないわけですから、クリックされても何も起こらないのです。これが、このデモで、新しく追加したli要素をクリックしても文字が赤くならない理由です。

では、新しく要素が追加されるたびに、その要素にも逐一、イベントを定義しなければならないのでしょうか。そのように実装を行うのもひとつの方法ではありますが、あとから追加された要素でも、すでに定義したイベントハンドラを使うことができる、便利な機能があります。

あとから追加する要素でもイベントハンドラを実行できるようにするには、すでに存在している祖先要素を起点としてイベントの定義を行い、その第二引数としてイベントの発生原となるセレクタ(あとから追加する要素)を指定します。

次の例では、親要素であるul要素を起点としてイベントを定義し、子要素であるli要素を監視するよう第二引数に指定しているため、あとから追加したli要素をクリックした場合も、イベントハンドラを実行できます。

あとから追加した要素にもイベントハンドラが実行される
$('ul').on('click', 'li', function() {
  $(this).css('color', 'red');
});

このように、イベントを定義する方法としては、はじめの例に出したように、要素に直接イベントを定義する方法と、特定の祖先要素を起点としてイベントを定義し、監視を任せる方法(デリゲートを用いたイベントの指定)の2種類があり、前者を「バインド」、後者を「デリゲート」という場合があります。

デリゲートを実現するための仕組み

jQueryのイベントは、イベントが発生した要素から外側の要素に向かって順番にイベントがバブリング(伝播)*していきます。

*注:JavaScriptネイティブイベントのバブリング

JavaScriptネイティブのイベントでもバブリングをしますが、IE8以下ではchangesubmitはバブリングしません。そのため、jQueryではネイティブのバブリングをラップし、IE8以下でもバブリングするように作られています。

イベントのバブリングイメージ

次の例では、ul要素に対してclickイベントを定義しています。ul要素の部分をクリックして、ul要素から発生するclickイベントを捕捉できるのは当然ですが、ul要素の子孫要素であるli要素やa要素の部分をクリックした場合も、それらから発生しバブリングしたclickイベントを捕捉できます。

バブリングしたイベントの捕捉
$('ul').on('click', function(e) {
  $target.text(e.target.tagName.toLowerCase());
  $currentTarget.text(e.currentTarget.tagName.toLowerCase());
});

.on()のデリゲートの仕組みは、このバブリングを利用しており、次のような手順で実行されます。

  1. デリゲートで指定した祖先要素に属する子孫要素からのイベントがバブリングで伝わってきたら、第二引数に指定されたセレクタを子孫要素内で検索する。
  2. マッチした要素の中から、イベントが発生した要素(event.target)と一致するものがあれば、祖先要素を起点として定義していたイベントハンドラが実行される。

祖先要素に属する子孫要素がクリックされるたびにセレクタを検索するので、イベントを定義した時点では存在しない要素に対してもイベントハンドラを実行することができるのです。

デリゲートで複数のイベントハンドラを定義する

イベントの発生源となる要素に対して直接イベントを定義する場合、複数のイベントハンドラを定義するには、.on()の第一引数にイベントタイプをキー、イベントハンドラを値としたオブジェクトを指定するのでした。

デリゲートで複数のイベントハンドラを定義するには、祖先要素に対してオブジェクトの形式でイベントを定義し、第二引数に監視する子孫要素のセレクタを指定します。

次の例では、「マウスオーバー時に文字色を赤くし、マウスアウト時に文字色を黒くする」というイベントハンドラを、祖先要素であるul要素を起点として定義し、li要素のイベントを監視しているため、あとから追加したli要素でもイベントハンドラが有効になります。

デリゲートで複数のイベントハンドラを定義
$('ul').on({
  'mouseenter': function() {
    $(this).css('color', 'red');
  },
  'mouseleave': function() {
    $(this).css('color', 'black');
  }
}, 'li');

バインドとデリゲートの使い分け

デリゲートはあとから追加する要素に対してもイベントを定義できるので便利ですが、すべてをデリゲートで記述してしまうのは問題です。

デリゲートは、祖先要素でイベントが発火するたびに、CSSセレクタを検索するので、バインドよりも処理に負担がかかります。

あとから要素がぽこぽこ増えるものや、イベントを定義したい要素の数が極端に多いものなど、デリゲートにする必要がある場合のみ用いるとよいでしょう。

コラム:.bind().delegate()

jQuery1.7で.on()が実装されるよりも前は、別のイベント定義用のメソッドが使われていました。

現在は.on()を使うことが推奨されていますが、要素に直接イベントを定義するには.bind()、祖先要素にイベントを定義するデリゲートは.delegate()というメソッドを使うことで、.on()と同様の機能が実装できます。

それぞれ、次のように記述します。

// .on()でバインドする
$('button').on('click', function() {...});
// .bind()でバインドする
$('button').bind('click', function() {...});
 
// .on()でデリゲートする
$('ul').on('click', 'li', function() {...});
// .delegate()でデリゲートする
$('ul').delegate('li', 'click', function() {...});

.bind()の引数は.on()との違いはほとんどありませんが、.delegate()の引数は子孫要素のセレクタとイベントタイプの指定が.on()の場合と逆になるので注意が必要です。

これらも.click()などと同じく.on()へのショートカットメソッドですので、jQuery内部では結局.on()が呼ばれています。

.on()の場合、引数の渡し方によって、バインドになったりデリゲートになったりするため、コードを見たときにパッと見でわかりづらいという理由から、.bind().delegate()を使い続けるという人もいるため、覚えておくとよいでしょう。

ちなみに.live()というイベント定義メソッドもありましたが、これはjQuery1.7で廃止されたため、現在は利用できません。.live()documentを祖先要素としたデリゲートですので、$(document).delegate()で代用可能です。

まとめ

今回は、.on()を使う理由とともに、基本的な使い方を紹介しました。

次回は、引き続き基本的な使い方として.on()と似た.one()や、.on()で定義したイベントを解除する.off()について解説したのち、.on()をさらに活用するために必要な、イベントの名前空間やカスタムイベント、トリガーについて紹介する予定です。

山田 敬美
山田 敬美
フロントエンド・エンジニア

Web制作会社、自社サービス運営会社などでサービス企画、マークアップ・エンジニアの経験を積むも、退職して1年間専門学校に通う。プログラミングの基礎を学んだのち、2013年4月、ピクセルグリッドのフロントエンド・エンジニアとして入社。CSSの設計を得意とするが、JavaScriptも好き。 著書に『Sass入門 ~より効率的なCSSコーディング』(共著:技術評論社、2012年10月19日)や『CSS3デザインブック 仕事で絶対に使うプロのテクニック』(共著:MDN、2012年3月21日)などがある。2019年、退社。