JavaScript
jQuery

jQuery.Deferredを使って楽しい非同期生活を送る方法

More than 5 years have passed since last update.

jQuery version1.5で導入されたjQuery.Deferredは、無くてもコードを書けるけど、使えば少しコードが綺麗かつ見通しが良くなる、という機能。

無くても書けるという機能がなかなか使われないというのは世の常なので、jQueryクックブック(O'REILLY)の中でも言及されていない、なんとも寂しい状況だ。

ちょっとここらで一肌脱いでやるか、という趣旨で書き始めたら無駄に長くなった。

とりあえず使ってみたい、という人は下の方の「jQuery.Deferred自体の使い方」までジャンプするとよい。


jQuery.Deferredとはどういう場面で使うものなのか

コールバックを渡して非同期処理完了時にそれを呼び出してもらうような場面。

具体的には

$.get('hoge', function(data) {

/*
hogeからGETで取ってきて、取ってきた結果を第一引数(data)にいれて実行される関数
*/

});

知ってる人が多いとは思うが、以下のように書くのは間違い

var hoge;

$.get('hoge', function(data) {
hoge = data;
});
alert(hoge); // => undefined

undefinedとアラートされる理由は$.getのコールバック関数が実行される前にalertが呼ばれるため。

これくらいはJavaScriptを少し聞きかじった人でもだいたい知っているだろう。

この程度なら大した問題にならないと思うし、jQuery.Deferredをわざわざ使わなくてもなんとかなる。

しかしながら、上記の例はhoge一つだけだったのが、もう少し話を進めてhigeとhomuまで必要になったとしたらどうなるだろう?

以下のようにコールバックの雨あられとなってしまう。

$.get('hoge', function(hoge) {

$.get('hige', function(hige) {
$.get('homu', function(homu) {
// hoge, hige, homu を使った処理
});
});
});

僕的には既にアウトだが、百歩譲ってこれがぎりぎりセーフだとしても、ここから更に歩を進めて、必要なデータの数が任意個だったらどうすればよいのか。

$.get(arg1, function(data) {

// 任意回のコールバックのネスト????
});

加えて、これらの間に依存関係がないとしたら、順番順番に取得するのは時間のムダだ。並列的にに取ってきたいだろう。つまり、

-(get hoge)->|-(get hige)->|-(get homu)->|-(hoge hige homuを使った処理)->

じゃなく

-(get hoge)->|

-(get hige)->|-(hoge hige homuを使った処理)->
-(get homu)->|

という感じだ。

ここでさっそうと現れるのがjQuery.Deferred

具体的には次のように書く。

まずは小手調べとして、hoge, hige, homuが必要な場合。

$.when($.get('hoge'), $.get('hige'), $.get('homu'))

.done(function(hoge, hige, homu) {
// hoge, hige, homu を使った処理
});

そして、任意個のデータが必要な場合。namesという配列に必要なのが入っているとする。

deferredObjects = [];

for (var i = 0; i < names.length; i++) {
deferredObjects.push($.get(names[i]));
}
$.when.apply(null, deferredObjects)
.done(function() {
// arguments を使って必要な処理を書く
});

一つでもエラーがあった場合は他の処理をしたい時は次のようになる。

$.when.apply(null, deferredObjects)

.done(function() { /* arguments を使う */ })
.fail(function() { /* エラー処理 */ });


具体的な利用シーン

キャッシュがあればそちらを使い、無かったらリモートから取ってきてキャッシュに入れて、其の後、値を使って処理をしたい、という場合は次のような感じに書く。(stack over flowにあった例を拝借)

var cache = {};

var getData = function(name) {
return cache[name] || $.ajax(name, {
success: function (data) {
cache[name] = data;
});
};

$.when(getData('hoge'), getData('hige'), getData('homu'))
.done(function(hoge, hige, homu) { /* */ });

このサンプルの味噌はgetDataが返すのが$.ajax()だったら遅延が生じ、単なるオブジェクト(ここではcache[name])だったら、遅延が生じないということだ。

つまり、hogeとhigeとhomuのうち、キャッシュに乗っていないものだけ取ってきて、全部がそろった段階でdoneに渡されたコールバックが実行される。


jQuery.Deferred自体の使い方

上記のページがいろいろ書いてある。ここでは中身には踏み込まず、ぶっちゃけどうすれば使えるかだけ書く。


0 非同期処理を含んだ関数を用意する

var func = function () {

async(function () {
// 非同期関数 async に渡されるコールバック関数
});
};


1 jQuery.Deferred#promise()を返すようにする

var func = function() {

var dfd = $.Deferred(); // new はあってもなくても同じ
async(function() { /* callback */ });
return dfd.promise();
};

funcを実行すると、内部のasyncの結果を待たずにdfd.promise()が返される。

返される値がこれ以外だった場合は、自動的に resolve(返されたデータ) 相当として処理される。(resolveについては後述)


2 コールバックの中でresolveもしくはrejectを呼ぶ

var func = function() {

var dfd = $.Deferred();
async(function() {
// 何かの処理
success ? dfd.resolve(/* 渡したいオブジェクト */) : dfd.reject();
});
return dfd.promise();
};

終わり。簡単簡単。

勘の良い人なら分かると思うが、jQuery.ajaxは上記の仕様を満たすように設計されているため、jQuery.whenに渡すことができる。


3 実行する

func()

.done(function(data) {
// resolveが実行された場合
// data には上のdfd.resolveに渡したオブジェクトが入っている
})
.fail(function () {
// rejectが実行された場合
});

上の方で書いたようにwhenには複数渡せば並列実行できる。

$.when(func(), func(), func())

.done(function(data1, data2, data3) { /* 全部 resolve だった場合 */ })
.fail(function() { /* 一つでも reject だった場合 */ });

ざっとこんな感じ。

jQuery.Deferredを使って非同期コーディングを楽しみましょう。

続編

結局jQuery.Deferredの何が嬉しいのか分からない、という人向けの小話