裏jQuery - 特殊なTriggerを作ってみよう

カテゴリ
ブックマーク数
このエントリーを含むはてなブックマーク はてなブックマーク - 裏jQuery - 特殊なTriggerを作ってみよう
このエントリーをはてなブックマークに追加
こんにちは。開発部でインターフェースエンジニアをやっております油井(あぶい)です。ライブドアでは主にjavascriptを中心としたクライアントサイド側の開発をやっております。

今回は裏jQueryと題しまして、普段から単にユーザーとして使っているだけでは決して知ることができないjQueryの裏技を紹介したいと思います。

注意
  1. この記事で扱うjQueryは最新版の1.4で動かすことを前提としています(一つ前のバージョンである1.3.2でも動くことは検証済みです)。
  2. サンプルで使うjQueryセレクタの書き方は「jQuery」で統一しています。「$」に置き換えて読んでもらってもかまいません。

はじめに - jQueryで扱うイベントやトリガー

javascriptがふんだんに使われた画面遷移の発生しないウェブアプリケーションではブラウザ上で発生するイベントやトリガーをうまく扱いこなすということが必須となります。ここでいうイベントやトリガーとは例えば「ページのロードが完了した」や「ページ上でクリックイベントが発生した」などのことです。

// ページのロードが完了した
jQuery(function () {
alert('done!');
})

// ページ上のどこかがクリックされた
jQuery( document ).bind('click', function () {
alert('clicked me!');
});

これらのイベントはブラウザにあらかじめ用意された、言い方を変えると特別に何も手を加えなくても扱えるイベントであるといえます。jQueryはそれらを簡潔かつ複数のブラウザで同じように動く書き方を提供してくれているにすぎません。

そこから一歩進んで「ajax通信が終了した」や「アニメーションが終了した」などのイベントの扱い方はどうでしょうか。もちろんそれらはjQueryが提供する公の機能として、決められたオプションや引数を指定することによって簡単に扱うことができます。

// ajax通信が終了した
jQuery.post('[api]', {params}, function () {
alert('ajax finished');
}, 'json');

/*
アニメーションが終了した(h1タグの位置を上部から数えて100px左端から数えて200pxの位置まで1秒かけて移動させる)
*/
jQuery("h1").animate({
top : 100,
left : 200
}, 1000, function () {
alert('animation finished');
});

jQueryのajaxではさらに、ajaxSetupを使うことによって「ajax通信が開始した」「ajax通信が失敗した」などのajaxのライフサイクルに応じて発生するイベントを簡単に扱うことができるようになっています。

複数のajax通信、アニメーションの終了を検知する方法1(スマートでない方法)

ではそこからさらに一歩進んで「同時に実行された全てのajax通信が終了した」や「同時に実行された全てのアニメーションが終了した」さらに「同時に実行されたajax通信とアニメーションが終了した」となるとどうでしょうか。
残念ながら現行のjQueryでは複数のajax通信・アニメーションを同時に実行する機能は提供されていません。ということで単一のajax(またはアニメーション)を扱うときのように、オプションや引数を渡すだけで簡単に扱うことはできないということになります。
ではどうするのかというと、普通に考えられるのは、複数並べた単一のajax通信(またはアニメーション)のコールバックが呼ばれる度に実行数を示すカウンタをカウントアップしていき、トータル実行数と同じ数になったタイミングで指定したコールバックを呼ぶコードを書くことです。
言葉で説明すると難しいので、複数のajax通信を同時に実行して、それらの全てが終了したタイミングで指定したコールバック関数を呼ぶサンプルを以下に示します。

var total = 2,
count = 0,
callback = function () {
alert('done');
},
tid = setInterval(function () {
if ( total == count ) {
clearInterval(tid), ( tid = null ), setTimeout(callback, 0);
}
}, 200);
jQuery.post('/api/012', function () { count++ }, 'json');
jQuery.get('/api/xyz', function () { count++ }, 'json');

はい、これでうまくいきました!といきたいところですが、このやり方の弱点は、全てのajax通信が終了したということを知るために事前にリクエストの総数を知っておかなければならないことです。つまり同期を取りたいajax通信の総数を間違ってしまうといつまで経ってもコールバック関数が呼ばれないということになりかねません。

その弱点を補った、事前に総リクエスト数を知らなくてもいいサンプルは以下の通りです。

function multi_ajax (apis, callback) {
var total = apis.length,
count = 0,
tid = setInterval(function () {
if ( total == count ) {
clearInterval(tid), ( tid = null ), setTimeout(callback, 0);
}
}, 200);
apis.forEach(function (api) {
jQuery(api, function () {
count++;
}, 'json')
});
}

multi_ajax([
"/api/012",
"/api/xyz"
], function () {
alert("done");
});

はい、これで解決しました!、、といきたいところですが、このやり方の弱点は個別に違うオプションや通信方式(post, get, getScript, getJSON, ajax)を使いたいときに引数の指定が複雑になるとともに、それを扱う関数も複雑になることです。

では、複数のajax通信(またはアニメーション)が終了するタイミングをスマートに知る方法はないのでしょうか?

複数のajax通信、アニメーションの終了を検知する方法2(スマート?な方法)

前置きが長くなりました。ここからが本題です。結論からいいますと、jQueryには公にはされていない特殊な変数を内部に持っています。公にされていないというのは、普通に使う分には必要がない(公にすると逆に混乱を招く)、もしくは内部で行われているロジックを成立させるためだけに存在しているということです。つまりソースを直接読まないと知れない情報であるといえます。

これまで散々いってきたajax通信とアニメーションのステータスを示す特殊な変数は以下の通りです。

jQuery.active
 jQuery.ajaxが呼ばれる度にカウントアップされます。そして終了と同時にカウントダウンされます。

jQuery.timers
 アニメーションが発生する度にそのアニメーションを実行する関数が蓄えられます。そしてアニメーションが終了すると同時に該当する関数が削除されます。

つまり、これらの値を監視することによって、同時に実行されたajax通信、またはアニメーションの終了を検知することができるというわけです。

Triggerクラスを作成してみる

ということで実際に先ほどの変数を監視するTriggerクラスを作ってみました。どのライブラリにも依存していないのでコピペするだけで使うことができるようになっています。

var Trigger = (function () {
function isArray (obj) {
return Object.prototype.toString.call(obj) == "[object Array]";
}
function each (obj, callback) {
for ( var i in obj ) {
callback(i, obj[i], obj);
}
}
function map (ary, callback, thisObject) {
var res = [];
each(ary, function (i, val) {
res[i] = callback.call(thisObject,val,i,this);
});
return res
}
function every (ary, callback, thisObject) {
for(var i=0,len=ary.length;i<len;i++) {
if(!callback.call(thisObject,ary[i],i,ary)) return false;
}
return true
}

var tid = null;
function COND (statement, callback) {
if ( tid != null ) clearInterval(tid), tid = null;
tid = setInterval(function () {
(isArray( statement ) ? every(statement, function (_cond) {
return _cond();
}) : statement()) && ( clearInterval(tid), tid = null, setTimeout(callback, 0) );
}, 200);
};
var events = {
allAnimated: function () {
return !$.timers.length;
},
ajaxFullComplete: function () {
return !$.active;
}
}, triggers = {
multi: function (names, callback) {
return COND
(
isArray( names ) ? map(names, function (name) {
return events[name] || function () { return true };
}) : names, callback
);
},
util: {
regist: function (name, statement) {
triggers[name] = function (callback) {
COND(statement, callback);
}
return {
after: function (callback) {
triggers[name](callback);
}
}
}
}
};
each(events, function (name, statement) {
triggers.util.regist.apply(null, arguments);
});
return triggers;
})();

Triggerクラスの使い方

Triggerクラスの使い方は以下の通りです。

同時に実行された全てのajax通信が終了した後にcallbackを呼びたい場合
 Trigger.ajaxFullComplete(callback);

同時に実行された全てのアニメーションが終了した後にcallbackを呼びたい場合
 Trigger.allAnimated(callback);

同時に実行された全てのajax通信とアニメーションが終了した後にcallbackを呼びたい場合
 Trigger.multi(['ajaxFullComplete', 'allAnimated'], callback);

自分で新しくトリガーを作成したいとき
 Trigger.util.regist(name, cond); // Trigger[name](callback);

新しく作成したトリガーを続けて使用したいとき
 Trigger.util.regist(name, cond).after(callback);

Triggerクラスのサンプルコード

atomフィードを並列に読み込み、表示した後に総ロード時間を表示するサンプルコードを書いてみました。

サンプルでは
  1. jQuery本体の読み込みを検知するためのloaded_jQueryというトリガー
  2. 並列で動いているajax通信の終了を検知するためのTrigger.ajaxFullCompleteというトリガー
この二つを使用しています。

コードの下にあるボタンを押すと、サンプルコードを動かすことができます。

/* Triggerクラスは既に読み込まれているものとする*/
Trigger.util.regist('loaded_jQuery', function () { return typeof jQuery != 'undefined' })
.after(function () {
var blogs = ['techblog', 'ld_directors'],
start = +new Date;
Trigger.ajaxFullComplete(function () {
jQuery.each(blogs, function (i, blog_id) {
var total_time = ( +new Date - start ) / 1000;
jQuery("#load_status").html('ロード時間:' + total_time + '秒');
})
});
jQuery.each(blogs, function (i, blog_id) {
jQuery('<div id="' + blog_id + '_feed"></div>').appendTo("#feeds_box");
jQuery.get('/'+blog_id+'/atom.xml', function (xml) {
var entries = xml.getElementsByTagName('entry'),
mtitle = xml.getElementsByTagName('title')[0];
mtitle = mtitle.text || mtitle.textContent;
var entry, title, link, lists = [];
for ( var i = 0, l = entries.length; i < l; i++ ) {
entry = entries[i], title = entry.getElementsByTagName('title')[0], link = entry.getElementsByTagName('link')[0];
title = title.text || title.textContent, link = link.getAttribute('href');
lists.push('<li><a href="' + link + '" target="_blank">' + title + '</a></li>');
}
jQuery('#'+blog_id+"_feed").html( mtitle + '<ul>' + lists.join('') + '</ul>' );
}, 'xml');
});
});

var s = document.createElement('script');
s.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js';
document.body.appendChild(s);





終わりに

以上、裏jQueryと題しまして、複数のajax通信、アニメーションの終了を検知する方法とそれを扱うTriggerクラスを紹介しました。いかがでしたでしょうか。他にもjQueryのこんな裏技を知ってるよ!という方は是非とも教えてくださいね。

それでは今年もよろしくお願い致します。
レスポンス
コメント(2)
トラックバック(0)

このエントリーをはてなブックマークに追加