jQuery custom event 応用編

前置き

custom eventとは何か?(前置きの前置き)
  • ブラウザがサポートしているeventではない独自定義event。
    • clickとかはブラウザがサポートしているevent
  • ユーザのアクションやブラウザの状態等によって直接発火されることはない
    • click eventは、ユーザがマウスポインタを要素の上に移動後にマウスのボタンを押すと発火される
    • DOMContentLoaded eventは、ページのHTMLがダウンロード完了し、すべてパースされると発火される
  • jQueryではtrigger,triggerHandler methodでeventをユーザのアクション等に関係なく発火することが出来る
    • custom eventも発火出来る

以前The JUI 2009 Returnsで話をしました。
http://dl.dropbox.com/u/612874/slide/jui20090710/s6maker.html

前置き本編

eventの発火については、事前に登録されている関数(eventHandler)が実行されるくらいに思っている人も多いと思うが、
custom eventで通常のプログラム並かそれ以上の豊かな表現ができるという話をします。

普通のプログラムで出来ることをcustom eventでもやってみる

(通常の)関数呼び出し

http://jsdo.it/monjudoh/bps41-custom-event-function-call

uu.ready(function(){
  function fun(){
    uu.log('fun is called.');
  }
  fun();
  $('#target').bind('fun',function(ev){
    uu.log('fun is triggered.');
  });
  $('#target').trigger('fun');
});

一番基本的なものです。
function 関数名(){}で関数を定義し、関数名()で関数を呼び出すというのと同じような事は、
.bind(eventType,関数)で特定の要素にeventを貼りつけ、.trigger(eventType)で発火させるということで出来ます。

引数付き関数呼び出し

http://jsdo.it/monjudoh/bps41-custom-event-function-call-with-args

uu.ready(function(){
  function fun(a,b){
    uu.log('fun is called. @,@.',a,b);
  }
  fun('hello','world');
  $('#target').bind('fun',function(ev,a,b){
    uu.log('fun is triggered. @,@.',a,b);
  });
  $('#target').trigger('fun',['hello','world']);
});

.bind(eventType,関数)で渡す関数(callback関数)の第一引数はevent objectです。
.trigger(eventType)で発火させるとcallback関数の第一引数としてevent objectが渡されるわけです。
では第二引数以降はどうなっているのかというと、
第二引数に配列以外を渡すと、callback関数の第二引数としてそれが渡され、
第二引数に配列を渡すと、callback関数の第二引数以降に展開されて渡されます。

インスタンス変数へのアクセスを含むメソッドの呼び出し

http://jsdo.it/monjudoh/bps41-custom-event-method-call

uu.ready(function(){
  var obj = {
    prop : 'hogehoge'
    ,method : method
  };
  function method(){
    uu.log('method is called.@.',this.prop);
  }
  obj.method();
  $('#target').data('prop','hogehoge');
  $('#target').bind('method',function(ev){
    uu.log('method is triggered.@.', $(this).data('prop') );
  });
  $('#target').trigger('method');
});

ここまでは関数呼び出しっぽい事をする話でしたが、
今度はオブジェクトのメソッド呼び出しっぽい事をする話です。
custom eventでインスタンス変数っぽいものをどう扱うかというと、
jQueryのdata methodを使います。
data methodはDOM要素ごとに独立した連想配列を扱うものです。

Class

http://jsdo.it/monjudoh/bps41-custom-event-Class

function Class(){}
Class.prototype.method = method;
function method(){
  uu.log('method is called.@.',this.prop);
}


$('.clazz').live('method',function(ev){
  uu.log('method is triggered.@.', $(this).data('prop') );
});
uu.ready(function(){

  var obj1 = new Class();
  obj1.prop = 'hogehoge';
  obj1.method();
  var obj2 = new Class();
  obj2.prop = 'fugafuga';
  obj2.method();
  
  $('<div id="target1" class="clazz"></div><div id="target2" class="clazz"></div>').appendTo('body');
  
  $('#target1').data('prop','hogehoge');
  $('#target1').trigger('method');
  $('#target2').data('prop','fugafuga');
  $('#target2').trigger('method');
});

Classをどう再現するかという話。
これにはbindによる要素へのevent貼付けではなく、
live methodによるCSSセレクタへのeventの貼付けを使います。
あるCSSセレクタ(例えばclassセレクタ)に対してliveでeventを貼り付け、
その後そのCSSセレクタを満たす要素(例えばclass属性に該当classが含まれている)を作成するのは、
Classを定義し、そのインスタンスを生成するのに似ています。


liveについては以前BPStudyで発表しました。jQuery1.4aでのlive event/special event - 文殊堂

mixin

http://jsdo.it/monjudoh/bps41-custom-event-mixin

var module = {method:method};
function method(){
  uu.log('method is called.@.',this.prop);
}


$('.clazz').live('method',function(ev){
  uu.log('method is triggered.@.', $(this).data('prop') );
});
uu.ready(function(){

  var obj = {prop:'hogehoge'};
  try {
    uu.log("method追加前にmethod呼び出し");
    obj.method();
  } catch(e) {
    uu.log(e);
  }
  $.extend(obj,module);
  uu.log("method追加後にmethod呼び出し");
  obj.method();
  
  $('<div id="target">').appendTo('body');
  
  $('#target').data('prop','hogehoge');
  uu.log("addClass前にtrigger");
  $('#target').trigger('method');
  $('#target').addClass('clazz');
  
  
  uu.log("addClass後にtrigger");
  $('#target').trigger('method');
});

後付でobjectにmethod群を生やすmixinです。
事前に、liveでclassセレクタにeventを貼り付けている状態で、
対象の要素のclass属性に該当classを後付で追加してしまえば実現できます。
liveでeventを貼りつけたCSSセレクタを満たすようになるわけですから。

callback

http://jsdo.it/monjudoh/bps41-custom-event-callback

function WebApiWrapper(){}
WebApiWrapper.prototype.call = call;
function call(){
  // Timerで大体500ms後に非同期でcallbackを実行する
  // WebAPI等をXHRで叩いてcallbackを実行するのの代わり
  var self = this;
  setTimeout(function(){
    (self.callback || function(){})('result');
  },500);
}


$('.clazz').live('call',function(ev){
  var self = this;
  // Timerで大体500ms後に非同期でcallbackを実行する
  // WebAPI等をXHRで叩いてcallbackを実行するのの代わり
  setTimeout(function(){
    $(self).trigger('callback',['result']);
  },500);
});
$('.clazz').live('callback',function(ev,result){
  uu.log('callback is triggered.@.', result);
});
uu.ready(function(){

  var obj1 = new WebApiWrapper();
  obj1.callback = function(result){
    uu.log('callback is called.@.',result);
  };
  obj1.call();
  
  $('<div id="target1" class="clazz">').appendTo('body');
  
  $('#target1').trigger('call');
});

JavaScriptではAjax等callback関数を登録し、
何かのタイミングでそれが呼ばれるといったコードを頻繁に書きます。
これをcustom eventで再現するにはどうすればいいかというと、
そもそも、eventからしてcallback登録→何かのタイミングでそれを実行、
というものなので特に頑張ることはありません。

普通のプログラムでは出来ないことをやる

mixout

http://jsdo.it/monjudoh/bps41-custom-event-mixout

$('.clazz').live('method1',function(ev){
  uu.log('method1 is triggered.@.', $(this).data('prop') );
});
$('.clazz').live('method2',function(ev){
  uu.log('method2 is triggered.@.', $(this).data('prop') );
});
uu.ready(function(){
  $('<div id="target">').appendTo('body');
  
  $('#target').data('prop','hogehoge');
  uu.log("addClass前にtrigger");
  $('#target').trigger('method1');
  $('#target').trigger('method2');
  $('#target').addClass('clazz');
  uu.log("addClass後にtrigger");
  $('#target').trigger('method1');
  $('#target').trigger('method2');
  $('#target').removeClass('clazz');
  uu.log("removeClass後にtrigger");
  $('#target').trigger('method1');
  $('#target').trigger('method2');
});

mixinの項では要素にclassを振ることで、後付で要素とlive eventを紐付けました。
じゃあ逆に要素からclassを外してしまえば、紐付けを解除することが出来ます。


普通のプログラミングでのmixinはmethod群を持つmoduleを取り込んで、
そのmethod群を生やす訳ですが、その逆、
moduleが持つmethod群を再び外してしまうということが、
custom eventでは実現可能なわけです。

同名のn(≧0)個の関数の呼び出し

http://jsdo.it/monjudoh/bps41-custom-event-many-function-call

uu.ready(function(){
  uu.log("bind前にtrigger");
  $('#target').trigger('fun');
 
  $('#target').bind('fun',function(ev){
    uu.log('fun(1) is triggered.');
  });
  uu.log("1個bind後にtrigger");
  $('#target').trigger('fun');
 
  $('#target').bind('fun',function(ev){
    uu.log('fun(2) is triggered.');
  });
  $('#target').bind('fun',function(ev){
    uu.log('fun(3) is triggered.');
  });
  uu.log("追加で2個bind後にtrigger");
  $('#target').trigger('fun');
});

普通のプログラミングではある名前の関数を呼んだ場合に実行される関数は一つです。
同じ名前で複数の関数を定義することは出来ません(オーバーロードできてもシグニチャも名前に含めれば1つです)。
また、未定義の関数を呼ぶことは出来ません(method missing等でエラーを回避できる言語はあります)。


custom eventではこれが出来ます。
event handlerの登録とeventの発火なので当たり前です。
eventそのものとして見る分には当たり前ですが、
これを関数の定義と呼び出しとして捉えると、

  • 定義されているかどうかを気にせず安全に呼び出せる
  • 好きなだけ追加で定義できる
    • 普通の関数だったら元関数を変数にバックアップし、追加の処理と元関数の呼び出しという内容の関数で定義を上書きすることになる

というスーパー関数と見ることが出来るのです。

まとめ

custom eventでもClassやmixin module等、普通のプログラミングで使う構造をシミュレートできます。
eventとして見れば当たり前のことでも、別の捉え方をすると凄いことだったりします。
これらを生かせばコードの一部をcustom eventベースに書き換えることが、
コードの構造に縛られることなく行うことが出来るのです。