Greasemonkey 複数のGM_xmlhttpRequestを同期取りながら順番に呼び出す

Greasemonkey の GM_xmlhttpRequest で外部サイトのデータを取得するとき。 Aのページを取得して、その内容を元にBのページにリクエストをかけたいときに、普通に onload に書いていくとネストが深くなっていって、何がなんだかわからなくなってしまう。

ソースをちょっとキレイにするためにこんなの作ってみました。

先に使い方から

//
// 使い方の例
//

// チェーンオブジェクトを作る
var chain = new Chain();

// 「addRequestFunction」で他サイトと通信するジョブを追加
// 引数は、GM_xmlhttpRequest の引数に渡すデータを返す関数。ややこしい><
chain.addRequestFunction( function() { return {
    method: 'GET',
    url: 'http://twitter.com/statuses/public_timeline.json',
    onload: function( response ) {
        // this = chain
        this.public_timeline = eval( "(" + response.responseText + ")" );
    }
}});

// 「addFunction」で普通の関数を追加
chain.addFunction( function() {
    // ここでは既に public_timeline にデータがセットされてる。便利!
    // this 経由でアクセスできるよ。
    var div = document.createElement( "div" );
    var txt = document.createTextNode( this.public_timeline[0].user.name );
    div.appendChild( txt );
    document.body.appendChild( div );
});


// 先頭にいた人のユーザー情報を取得してみよう。
chain.addRequestFunction( function() {
    // ここからも this でアクセスできるよ
    var id = this.public_timeline[0].user.screen_name;
    
    // 先頭にいた人のユーザー情報を取得するジョブ
    return {
        method: 'GET',
        url: "http://twitter.com/users/show/" + id + ".json",
        onload: function( response ) {
            this.user = eval( "(" + response.responseText + ")" ); // this = chain
        }
    }
});

chain.addFunction( function() {
    var img = document.createElement( "img" );
    img.src = this.user.profile_image_url;  // this = chain
    document.body.appendChild( img );
});


// 「doChain」で追加してきた関数を次々と実行する。
chain.doChain();

ソース

//
// チェーンを順番に実行して行くクラス。
//
function Chain() {
    // コンストラクタ
    this.jobs = [];
    this.container = {};
    
    // チェーンに GM_xmlhttpRequest の引数を返す関数を追加する。
    this.addRequestFunction = function( f ) {
        this.jobs.push({ type: 'request', func: f })
    };
    
    // チェーンに普通の関数を追加する。
    this.addFunction = function( f ) {
        this.jobs.push({ type: 'function', func: f })
    };
    
    // 先頭のジョブを返す
    this.shift = function() {
        return this.jobs.shift();
    };
    
    // チェーンを順番に実行して行く。
    this.doChain = function () {
        // 先頭のジョブを取り出す。何もなかったらおしまい。
        var job = this.jobs.shift();
        if( ! job ) return;
        
        if( job.type == 'function' ) {
            // 関数ならそのまま実行する。
            job.func.apply( this );
            this.doChain();
        } else if( job.type == 'request' ) {
            // objectだったら非同期通信。ちょっといじってから実行する。
            var obj  = job.func.apply( this );
            obj.chain = this;
            if( obj.onload ) {
                obj.$onload = obj.onload;
                obj.onload = function( response ) {
                    obj.$onload.apply( this.chain, [response] );
                    this.chain.doChain();
                }
            }
            GM_xmlhttpRequest( obj );
        }
    };
}