サーバの負荷低減と可読性を考慮した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);
}