jQueryのDeferredオブジェクトについて調べてみた

はじめに

最近になって jQueryDeferred Object と呼ばれるものが追加されたことを知りました。バージョン1.5から追加になったようです。
ここのところ色々な場面で非同期処理と付き合うことが多かったこともあって調べてみたのですが、中々嬉しい機能を持っているやつであることが判明したので、調べた内容についてまとめてみました。

非同期処理は結構書きにくい

jQuery Deferred オブジェクトは非同期処理を書きやすくするために用意されたものです。でも何でそんなものが追加されたのでしょうか?
非同期処理は複雑になってくると書きにくいものです。Ajaxリクエストを例に挙げてみます。

$.ajax({
  url: "serviceA.json",
  success: function(data) {
    // リクエストが成功したときの処理
  },
  error: function(xhr, status, error) {
    // 失敗したときの処理
  }
});

非同期処理の典型的な書き方ですね。jQuery.ajax() メソッドを使って serviceA.json というURLにリクエストを書けますが、レスポンスが返ってくるまで待ちません。(ajax() メソッドをコールした後制御が戻ってくる)
レスポンスが返ってきたら処理を依頼した相手に呼び戻して (コールバックして) もらいます。そこでコールバック関数を渡しておき、その中にリクエストが成功もしくは失敗した際にその結果を受け取って行う処理を実装することになります。この例では ajax() メソッドの引数に渡したオブジェクトの success プロパティにセットした関数、もしくは error プロパティにセットした関数です。

これだけなら単純ですが、ここで serviceB.json というWebサービスのURLがあって、そのURLには serviceA.json から取得した結果をパラメータとして渡したいというケースはどうでしょう?
ワークフローとしては次のようになります。

これをコードに書くとこんな風になります。

$.ajax({
  url: "serviceA.json",
  success: function(data) {
    $.ajax({
      url: "serviceB.json",
      data: {param: data.param},
      success: function(data) {
        // serviceB.json へのリクエストが成功したときの処理
      }
    });
  },
  error: function(xhr, status, error) {
    // 失敗時の処理
  }
});

一気に見にくくなりましたね...。無名関数を使わず、名前付き関数をコールバックに渡すようにすれば少しましになりますが、それでもどんな順番で処理が呼び出されるかがパッと見分かりにくいです。

さらに、serviceC.json というWebサービスURLにもアクセスする必要があるが、これは他のURLとは依存関係が無い。このURLからのデータ取得と、serviceA.json -> serviceB.json でのデータ取得が完了したタイミングで次の処理を行いたい、なんてことになった場合にはどうなるでしょう...。

こうなるとそれぞれの処理の中でフラグを管理するような記述が必要になり、どんどんソースがややこしくなってしまいます。*1

jQuery に新たに追加された Deferred オブジェクトを利用すると、このような非同期処理の組み合わせをすっきり書けるようになります。

jQuery Deferred オブジェクトとは?

一言でまとめると、非同期処理に対する (複数の) コールバックのキューを管理するオブジェクトと言ったところでしょうか。このオブジェクトを利用すると次のようなことを容易に記述出来るようになります。

  • 処理が成功時のコールバック、失敗時のコールバックを別々に管理する
  • ある非同期処理が終わったら次の非同期処理にパイプする
  • 複数の並列で走っている非同期処理がすべて完了したときに次の処理を実行する

まずは簡単なコード例を以下に示します。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<script src="jquery-1.6.js"></script>
<script>
$(function() {
    var df = $.Deferred();
    var output = $("#output");
    df.then(
        function(arg) {
            output.append("成功側のコールバックが呼ばれました。もらった引数は [" + arg + "] です。");
        },
        function(arg) {
            output.append("失敗側のコールバックが呼ばれました。もらった引数は [" + arg + "] です。");
        }
    ).done(function(arg) {
        output.append("成功時のコールバックをもう一丁登録。");
    }).always(function(arg) {
        output.append("どっちでもコールされるはずです。");
    });
    $("#btnResolve").click(function() {
        df.resolve("成功したよ");
    });
    $("#btnReject").click(function() {
        df.reject("失敗したよ");
    });
});
</script>
</head>
<body>
<p>
<button type="button" id="btnResolve">Resolveを呼び出します</button>
<button type="button" id="btnReject">Rejectを呼び出します</button>
</p>
<p id="output"></p>
</body>
</html>
  • jQuery.Deferred() が Deferred オブジェクトのファクトリメソッドになります。
  • Deferred オブジェクトにはコールバックを登録するためのメソッド (上の例では then, done, always) やコールバックをキックするためのメソッド (上の例では reject, resolve) があります。
  • Deferred のコールバックを登録するメソッドは自分自身をリターンするので、上の例のように登録メソッドをチェインすることでコールバックを複数登録することが可能です。コールバックは登録順に呼び出されます。

非同期処理を提供する側は Deferred オブジェクトを作って利用側に渡し、利用側が受け取った Deferred オブジェクトに対して用意されたメソッドを用いてコールバックを登録することになります。そして、非同期処理を提供する側は自身の処理が完了したらその処理結果に応じて Deferred オブジェクトに対してコールバックをキックするメソッド (成功したときに呼び出すもの、失敗したときに呼び出すものそれぞれあります) をコールすることになります。
以下、Deferred オブジェクトに用意されたメソッドや関連するトピックについての説明をまとめてみます。

コールバックをキックするためのメソッド

こちらは Deferred オブジェクトに用意された、コールバックをキックするためのメソッドです。非同期処理を提供する側が呼び出すことになります。
大きく分けてresolve系のメソッドreject系のメソッドがあり、前者を呼び出すと成功時に呼び出すように登録されたコールバックがキックされ、後者を呼び出すと失敗時に呼び出すように登録されたコールバックがキックされます。

メソッド 説明
resolve(args) 成功したときにキックするメソッド。done() に登録、もしくは then() の第1引数に登録されたコールバックが呼び出される。このメソッドの引数はコールバックの引数に渡される。
resolveWith(context, [args]) 成功したときにキックするメソッドだが、第1引数にはコールバックの this に設定されるオブジェクトを渡す。つまりコールバックはこの引数のメソッドとして呼び出される。第2引数は配列を渡すことに注意! (内部で Function.prototype.apply が呼び出される)
reject(args) 失敗したときにキックするメソッド。fail() に登録、もしくは then() の第2引数に登録されたコールバックが呼び出される。このメソッドの引数はコールバックの引数に渡される。
rejectWith(context, [args]) 失敗したときにキックするメソッドだが、第1引数にはコールバックの this に設定されるオブジェクトを渡す。

後ろに "With" とついた名前のメソッドは、コールバックを何か特定のオブジェクトのメソッドとして実行させたい時に使います。

コールバックを登録するためのメソッド

対してこちらはコールバックを登録するためのメソッドです。上でも書きましたが、これらのメソッドは自分自身をリターンするので、いくらでもチェインして複数のコールバックを登録することが可能です。

メソッド 説明
done(callback, [callback]) resolve() がコールされた際のコールバック関数を登録する。複数渡すことが可能。
fail(callback, [callback]) reject() がコールされた際のコールバック関数を登録する。
then(doneCallback, failCallback) こちらは第1引数に resolve() がコールされた際の、第2引数に reject() がコールされた際のコールバックを登録する。
always(callback) resolve()、reject() どちらでもコールされるコールバック関数を登録する。finally的な処理を登録するのに使える。

成功時の処理は done、失敗時の処理は fail、まとめて登録するときは then、成功、失敗関係なしに同じ処理を実行するときは always で登録する、と覚えておけばいいでしょう。

Promise とは?

Deferred にはさらに promise() というメソッドが用意されています。これは Deferred オブジェクトから resolve(), reject() メソッドを取り除いた Promise というオブジェクトを返却するためのメソッドです。
非同期処理を利用する側に対しては Promise を返すようにして、利用側から勝手に resolve とか reject を呼ばれないようにするわけですね。

Deferred オブジェクト導入に伴う Ajaxメソッドの変更

Deferred オブジェクトの導入に伴い、jQuery.ajax() や jQuery.get() などの Ajax 系のメソッドにも変更が入りました。
1.5より前は XMLHttpRequest (以下 XHR) オブジェクトを返していましたが、1.5 からは jqXHR というオブジェクトを返すように変更されています。
このオブジェクトは XHR オブジェクトのスーパーセットで、XHR と同じインターフェースとなっていますが、加えて Promise のインターフェースも備えています。よって次のように jQuery.ajax() の戻り値に対して、done や fail を使ってコールバックを登録出来るようになるわけです。

var jqxhr = $.ajax({ url: "example.json" });
jqxhr.done(function() { // 成功時の処理 }).fail(function() { // 失敗時の処理 });

jQuery.ajax() だけでなく、jQuery.get() や jQuery.post() もこの jqXHR オブジェクトを返すようになっています。これらのメソッドはリクエストに失敗したときのコールバックが登録できず、個人的には使いものにならない印象を持っていたのですが、これでやっと使えるようになりました。

Deferred オブジェクトを使って非同期処理のパイプや待ち合わせを実装してみる

では、[非同期処理は結構書きにくい] のところで挙げた例を Deferred オブジェクトを使って書きなおしてみることにします。
まずは以下の例から。

このように先に実行した非同期処理が完了してから次の非同期処理を実行する (パイプする) 場合には、Deferred.pipe() メソッドを使います。
コードは次のようになります。

var task1, task2, param;
task1 = $.getJSON("serviceA.json").then(
  function(data) {
    param = data.param;
  },
  function(data) {
    // serviceA.json へのリクエストが失敗したときの処理
  }
);
task2 = task1.pipe(
  function() {
    return $.getJSON("serviceB.json", {param: param});
  },
  function() {
    // 失敗しているときは後続を実行したくない場合、このように Deferred を返さずに何らかのエラーを示すものを返すと良い
    return "failed";
  }
).then(
  function(data) {
    // serviceB.json へのリクエストが成功したときの処理
  },
  function(data) {
    // serviceB.json へのリクエストが失敗したときの処理
  }
);

pipe() メソッドは引数を2つ持ち、最初の引数には resolve されたときのフィルタ関数を、2番目の引数には reject されたときのフィルタ関数を渡します。このフィルタ関数は Deferred もしくは Promise オブジェクトを返すことが出来ます。つまり、非同期処理のパイプが可能となります。
上の例では、serviceA.json へのリクエストが成功したときには、新たに serviceB.json に対する Ajax リクエストを実行し、その返り値 (jqXHR オブジェクト、つまり Promise オブジェクト) を返却して、処理をパイプするようにしています。
最初に書いた例と比べて結構見やすくなったのではないでしょうか?

では、こちらの例を書いてみることにします。こちらは複数の Deferred オブジェクトの待ち合わせを行う jQuery.when() メソッドを用いて書くことが出来ます。

コードはこのようになります。(先程のコード例の続きとなるので、併せて見てください)

var task3 = $.getJSON("serviceC.json").then(
  function(data) {
    // serviceC.json へのリクエストが成功したときの処理
  },
  function(data) {
    // serviceC.json へのリクエストが失敗したときの処理
  }
);
$.when(task2, task3).then(
  function(argsFromTask2, argsFromTask3) {
    alert("全部処理が終わりました!");
    // 何か後続の処理
  },
  function(argsFromTask2, argsFromTask3) {
    alert("途中でこけたやつがあります!");
  }
);

jQuery.when() メソッドは、(基本的に) 引数に Deferred オブジェクトを取り、引数に渡された Deferred オブジェクトが全て resolve もしくは reject されたときに、このメソッドが返す Promise オブジェクトの resolve もしくは reject をキックします。
従って、これを使うことで複数並行に走っている非同期処理の待ち合わせをこんなに簡単に書くことが出来るというわけです。
ちなみに when の返す Promise オブジェクトにセットしたコールバックの引数には、when() の引数に渡した全ての Deferred から渡される引数が順番に配列で渡されます。(上のコード例では引数名を分かりやすいように "argsFromTask2"、"argsFromTask3" と書いています)

まとめ

jQuery1.5 になって追加された Deferred オブジェクトを利用すると、書きにくい非同期処理が随分すっきりと書けるようになります。
非同期処理を書きやすくしようとするライブラリは他にもありますが、幅広く使われている jQuery の標準機能として追加されたことは大きいのではないでしょうか。

個人的にはついこの前まで、Flex アプリの開発でこのような複数の非同期処理のパイプや待ち合わせ処理を実装していたので、この機能のありがたみがすごくよく分かりました。というか ActionScript にもこんなライブラリが欲しかった...。

*1:外部Webサービスを利用するRIAを作っていると、このような実装をすることはザラにあります。