jQuery.Deferredとは

jQueryバージョン1.5から加わった機能で、非同期処理便利に行うために用意されたものです。
$.ajaxなどをサンプルを見ると、書き方が変わったことが分かります。

jQuery バージョン1.5より前

$.ajax({
    url: 'data.html',
    success: function(data) {
        console.log('success');
    },
    error: function(data) {
        console.log('error');
    }
});

jQuery バージョン1.5以降

$.ajax( 'data.html' )
.done(function() {
    console.log('success');
})
.fail(function() {
    console.log('error');
});

同期と非同期

同期非同期とは何でしょう?

同期は、処理を実行したら、結果・完了が返ってくるのを待って、次の処理を実行します。

非同期は、処理を実行したら、結果・完了を待たずに、次の処理を実行します。

特にJavaScriptでは、非同期で処理が行われる場合が多く、コールバックを多用して、次の処理を実行します。

コールバック地獄

非同期での処理が多くなると、コールバックによって入れ子が深くなり、コードの視認性・可読性が悪くなり、あまり良くありません。

成り行きで、入れ子の深いコードを書くことは、簡単ですが、自分で書いたコードでも、二度と触りたくないものになることがあります。

ECMAScript6で実装されるyield(イールド)というものを使うと、このコールバックの地獄から逃れることができるようですが、ブラウザでの実装が追いついていない状況です。

というわけで、jQuery.Deferredを使って、非同期処理を書こうと思います。

とりあえずアニメーションでやってみた

簡単なアニメーションでは、あまり恩恵は得られませんが、やってみました。
jQueryのanimatecompleteを利用して、順番に動かします。
普通にやるとこんな感じでしょうか。

コールバックを入れ子

function animeTest1(){
    var $target = $('#wrapper>p');

    $target.eq(0).animate({'height':'1em','top':'10px'},1000,function(){
        $target.eq(1).animate({'left':'10px','top':'90px'},1000,function(){
            $target.eq(2).animate({'left':'10px','top':'120px'},1000,function(){
                $target.eq(3).animate({'top':'180px'},1000,function(){
                    $target.eq(4).animate({'left':'10px','top':'260px'},1000,function(){
                        $target.eq(5).animate({'left':'10px','top': '300px'},1000,function(){
                            alert('完了');
                        });
                    });
                });
            });
        });
    });
}
$(document).ready(function(){
    animeTest1();
});

サンプル – コールバックを入れ子

deferredを使ったパターン1

function animeTest2(){
    var $target = $('#wrapper>p');

    var promiseFunc = function(action){
        var d = $.Deferred();
        action(d);
        return d.promise();
    };

    promiseFunc(function(d){
            $target.eq(0).animate({'height':'1em','top':'10px'},1000,d.resolve);
    }).then(function(){
        return promiseFunc(function(d){
            $target.eq(1).animate({'left':'10px','top':'90px'},1000,d.resolve);
        });
    }).then(function(){
        return promiseFunc(function(d){
            $target.eq(2).animate({'left':'10px','top':'120px'},1000,d.resolve);
        });
    }).then(function(){
        return promiseFunc(function(d){
            $target.eq(3).animate({'top':'180px'},1000,d.resolve);
        });
    }).then(function(){
        return promiseFunc(function(d){
            $target.eq(4).animate({'left':'10px','top':'260px'},1000,d.resolve);
        });
    }).then(function(){
        return promiseFunc(function(d){
            $target.eq(5).animate({'left':'10px','top': '300px'},1000,d.resolve);
        });
    }).always(function(){
        alert('完了');
    });
}
$(document).ready(function(){
    animeTest2();
});

サンプル – deferredを使ったパターン1

行数的には長くなりました。あまりイケていません。
調べてみると、ajaxだけでなく、animateなどもdeferredオブジェクトが使われているようです。

わざわざ、自分で$.Deferred()しなくても良いようです。

deferredを使ったパターン2

function animeTest3(){
    var $target = $('#wrapper>p');

    $target.eq(0).animate({'height':'1em','top':'10px'},1000).promise()
    .then(function(){
        return $target.eq(1).animate({'left':'10px','top':'90px'},1000);
    }).then(function(){
        return $target.eq(2).animate({'left':'10px','top':'120px'},1000);
    }).then(function(){
        return $target.eq(3).animate({'top':'180px'},1000);
    }).then(function(){
        return $target.eq(4).animate({'left':'10px','top':'260px'},1000);
    }).then(function(){
        return $target.eq(5).animate({'left':'10px','top': '300px'},1000);
    }).always(function(){
        alert('完了');
    });
}
$(document).ready(function(){
    animeTest3();
});

サンプル – deferredを使ったパターン2

先ほどよりもスッキリしましたね。
これなら実務でも簡単に使えそうです。

$.when

$.whenを使うと、複数のdeferredオブジェクトを監視することができます。

whenを使ったパターン

function animeTest4(){
    var $target = $('#wrapper>p');

    function anime1(){
        return $target.eq(0).animate({'height':'1em','top':'10px'},1000).promise()
        .then(function(){
            return $target.eq(1).animate({'left':'10px','top':'90px'},1000);
        }).then(function(){
            return $target.eq(2).animate({'left':'10px','top':'120px'},1000);
        });
    }

    function anime2(){
        return $target.eq(3).animate({'top':'180px'},1000).promise()
        .then(function(){
            return $target.eq(4).animate({'left':'10px','top':'260px'},1000);
        }).then(function(){
            return $target.eq(5).animate({'left':'10px','top': '300px'},1000);
        });
    }

    $.when(
        anime1(),
        anime2()
    ).done(function(){
        alert('完了');
    });
}
$(document).ready(function(){
    animeTest4();
});

サンプル – whenを使ったパターン

まとめ

jQuery.Deferredは、deferredオブジェクトを作成(もしくはdeferredオブジェクトが作成される機能を使用)し、promiseを返すとdeferredを使う準備ができます。

準備後は、thenを使って、チェーンメソッドで、処理を次々実行させることができます。

thenは自動でpromiseを返すので、thenを連結させていくと、簡単に記述できます。

jQueryのanimate系は、アニメーション終了時に自動的にresolveしてくれるようで、こちらも意識しなくても良いようになっています。

これで、非同期ライフが送れそうです。
そんじゃーねー。