jQuery の Deferred でのエラー処理 その(2)

この内容は jQuery 1.7.2 を元に記述しています。

jQuery の Deferred でのエラー処理 その(1)」では基本的なエラー処理をみて、「jQuery の Deferred の then について」では then がただ単純にコールバック関数を登録してるだけってのが確認できました。

ここでは、deferred を繋げて実行する pipe でのエラー処理を確認してみます。

基本的な使い方は以下のような感じ。

async("Test 1", 1000).pipe(function(e){
    return async("Test 2", 500);
}).pipe(function(e){
    return async("Test 3", 1000);
}).done(function(e){
    console.log("done - " + e);
});

結果は以下。

f:id:k_maru:20120623202013p:plain

この時点で個人的には頭を抱えてしまいそうなんですが、最後の結果だけが返ってきてるようです。先の2つの結果はどこえ消えたのか・・・。もちろん、引数が複数になっているわけではありません。仮に複数になってても第1引数になぜに最後の結果なのか、こっちも頭を抱えてしまいますが。。

調べてみると、どうやらそれぞれの結果は pipe に登録するコールバック関数の引数に渡ってくるようです。

async("Test 1", 1000).pipe(function(e){
    console.log("pipe - " + e);
    return async("Test 2", 500);
}).pipe(function(e){
    console.log("pipe - " + e);
    return async("Test 3", 1000);
}).done(function(e){
    console.log("done - " + e);
});

f:id:k_maru:20120623202025p:plain

まぁ、これはこれで分かるのですが、単純に pipe は「次の処理を行う」のではなく、「結果を渡した際に、次の処理を依頼(deferred/promiseが返却)されたら実行する」というような、動きのようです。

ここからエラーの際の動きを確かめてみますが、上記まででなんとなくどういう動きをするのかは予想がつきますね。

async("Test 1", 1000).pipe(function(e){
    /* ここで reject */
    return async("Test 2", 500, true);
}).pipe(function(e){
    return async("Test 3", 1000);
}).done(function(e){
    console.log("done - " + e);
}).fail(function(e){
    console.log("fail - " + e);
});

f:id:k_maru:20120623203117p:plain

2つ目(Test 2)で reject してるのですが、後続する pipe にエラー時のコールバックが登録されていないため、そのままなにも行われず、 fail のコールバックが実行されています。ここで、間違ってはいけないのは「エラーが起こったら、後続する pipe が飛ばされる」のではないってところですかね。ちょっと確認してみます。

/* 先頭で reject */
async("Test 1", 1000, true).pipe(function(e){
    return async("Test 2", 500);
}, function(e){
    console.log("errorback - Test2 " + e);
}).pipe(function(e){
    return async("Test 3", 1000);
}, function(e){
    console.log("errorback - Test3 " + e);
}).done(function(e){
    console.log("done - " + e);
}).fail(function(e){
    console.log("fail - " + e);
});

f:id:k_maru:20120623203947p:plain

後続する pipe にエラー時のコールバックが指定されている場合は、ちゃんと実行されていますね。ただ、さらによく見てみると2つ目以降の以降のコールバックの値が undefined になってます。これは1つ目で値を返してないからですね。ちゃんと reject で値を指定して返さないといけないようです。

/* 先頭で reject */
async("Test 1", 1000, true).pipe(function(e){
    return async("Test 2", 500);
}, function(e){
    console.log("errorback - Test2 " + e);
    return $.Deferred().reject(e);
}).pipe(function(e){
    return async("Test 3", 1000);
}, function(e){
    console.log("errorback - Test3 " + e);
    return $.Deferred().reject(e);
}).done(function(e){
    console.log("done - " + e);
}).fail(function(e){
    console.log("fail - " + e);
});

f:id:k_maru:20120623204643p:plain

もう、どれがどのコールバックなのか判別つきません。。ネストが深いのとどっちがうれしいんだか。

エラーのコールバックは登録せずに fail に任せればいいのかもしれません。が、それだと、今回の場合みたいに渡される引数の値でどの処理なのかが判別できない場合、どこでエラーが発生したのか判別できません。なので、そういう場合はやはりエラーのコールバックはそれぞれに登録する必要がありそうです。さらにそうした場合、手前で発生したエラーなのか、さらにそれ以前で発生したのかを判別する必要も出てきそうです。さらにさらに、エラーが発生した前までの結果データをもとになにか行いたい場合(補償トランザクションとか画面の再描画とか)には、結果データの保持も必要そうです。ちょっと書いてみました。

var results = [];
/* 先頭で reject */
async("", 1000, true).pipe(function(e){
    results.push(e);
    return async("", 500);
}, function(e){
    return $.Deferred().reject({ errorpos: 1, data: e });
}).pipe(function(e){
    results.push(e);
    return async("", 1000);
}, function(e){
    var result = e;
    if(!e || !e.errorpos || e.errorpos < 1){
        result = { errorpos: 2, data: e };
    }
    return $.Deferred().reject(result);
}).done(function(e){
    var i = 0, length = results.length;
    results.push(e);
    for(; i < length; i++){
        render(results[i]); //何かの処理
    }
}).fail(function(e){
    var i = 0, length = results.length;
    showerror((e.errorpos && e.data) ? e.data : e);
    for(; i < length; i++){
        revert(results[i]); //何かの処理
    }       
});

イメージコードなので、あってるかどうか確証はないですが、結構大変そうです。

ということで、pipe での嬉しさがでるのは「エラーが起こっても、単純に起こったことが取れれば OK で、どこで起こったのか、起こるまでの結果は不要」なときくらいでしょうか。もうちょっとうまいやり方(あくまでも jQuery 標準の API を使って)あってもいいのかなと思うのですが、無いんでしょうか。。。