サーバの負荷低減と可読性を考慮したAjaxコーディング
はてなブックマークの「おすすめ」を求めるブックマークレットでは、「はてなブックマーク」から複数回RSSファイルなどを取得します。この時、サーバへ大きな負荷を与えないよう、1つのファイルのダウンロードが終了してから、次のファイルをダウンロードするようにしています。
複数ファイルをダウンロードするAjaxの処理をコーディングするのは意外と面倒です。単純に書くならば、ファイルのダウンロード終了後の実行するイベントハンドラとして、次のファイルをダウンロードする関数を与えることになります。しかし、ダウンロードするファイル数が多数の場合、一つ一つ関数を定義するのは明らかに無駄です。こういった場合、JavaScriptでは関数がオブジェクトである点を利用して、関数オブジェクトを返す関数を定義するのが定石です。
例えば、次のようなコードが考えられるでしょう。なお、prototype.jsを前提にしています。また、コンストラクタなどは省略しています。
var Recommender = Class.create();
Recommender.prototype.extend({
analyzeEachEntry: function(i){
return function(){
if(i == 0){
this.logger.print("Analyzing entries ...");
}
if(i != this.entries.length){
this.logger.print(".");
this.entries[i].fetch(this.analyzeEachEntry(i + 1).bind(this));
}else{
this.logger.print("done.");
}
}
},
analyzeEntries: function(){
this.analyzeEachEntry(0).bind(this)();
}})analyzeEachEntryメソッドは、「i番目のエントリーをダウンロードする関数オブジェクト」を返すメソッドです。i番目のエントリーをfetchメソッドでダウンロードする際には、「i+1番目のエントリーをダウンロードする関数オブジェクト」を、ダウンロード終了後のイベントハンドラとして与えています。
analyzeEntriesメソッドで、this.entries配列に格納されているエントリーのダウンロードを開始します。具体的には、「0番目のエントリーをダウンロードする関数オブジェクト」を生成して、この関数オブジェクトを実行しています。
このように処理を記述する利点には、次の2点があると思います。
- 初期化処理や、終了処理を一つのメソッド内でまとめて書けるため、可読性が高い。
- イベントハンドラとして、同じメソッドを与えることで、同一クラス内に全ての処理がまとまり、可読性が高い。
なお、this.analyzeEachEntry(i).bind(this)という処理は、ハンドラ内に記述されたthisの参照先を、現在の文脈でのthisにする処理です。JavaScriptではthisの参照先が実行時に決定されます。そのため、この処理を加えないと、ハンドラが呼び出された時の文脈に従って、ハンドラ内のthisが解釈されてしまいます。個人的には、bindメソッドは、prototype.jsの中でも頻繁に用いるメソッドの一つで、だいたいthis.hogeHandler.bind(this)という使い方をします。
また、参考までにEntryクラスのfetchメソッドを示しておきます。
fetch: function(handler){
var option = {
onComplete: function(request){
this.html = request.responseText;
if(handler){
handler();
}
}.bind(this);
}
new Ajax.Request(this.entry_url, option);
}