jQuery deferredの使い方 前編 deferredの基本
jQuery deferredをなんとなく知っているが使い方がよくわからない人のために短期シリーズでおさらいをします。第1回目は非同期処理の概念からdeferredの基本機能までをおさえます。
- カテゴリー
- JavaScript >
- jQuery
発行
なぜ、今、jQuery deferred?
この短期シリーズでは、2回に分けてjQuery deferredについて解説します。
jQuery deferredは、jQuery1.5で追加された機能であり、特に新しいものではありません。
しかしながら、私が昔書いたjQuery deferredに関するブログ記事をブックマークする人が、いまだにちょこちょこいるのが見受けられるのです。
もしかして、deferredの存在はなんとなく知っているけれど、どのように使ったらいいのかよくわからないという人が多いのかもしれないと思い、改めてまとまったものを書いてみることにしました。
本シリーズでは、jQuery deferredの基本的な部分と、どのような場面で使うのかという2点について解説します。
このシリーズのサンプルは、以下のレポジトリに置いてあります。
jQuery deferredサンプル集
なお、この記事は、jQuery version 1.9.1の時点で書かれたものです。
jQuery deferredとはなにか
まずjQuery deferred(以降、deferred)とはなんなのかということから始めましょう。
deferredはjQueryに備わっている機能のひとつです。ですから、jQuery以外のなにか特別にプラグインのようなものを追加で読み込まなくても使えます。jQueryの機能の大部分は、DOMを操作するものとなっていますが、deferredはそうではありません。ざっくり言ってしまえば、deferredは、非同期処理を円滑に行うための、お助けツールのようなものです。deferredの説明はjQuery API Documentationの以下のページにあります。
deferredの説明
しかしながら、このページを読んでみても、よくわからないかもしれません。私がこのページを初めて見たときは、そのように感じました。そこで具体的なサンプルを見つつ、理解を深めていくこととしましょう。
非同期とはどういうことか
deferredについて解説する前に「非同期」という言葉について説明しておきます。既知の方は適当に読み飛ばしてください。
JavaScriptを書いていく上で、非同期な処理を扱うのは避けて通れません。その非同期な処理というものとはどういうものなのか。筆者が例としてパッと思い浮かんだのは、setTimeout
と、$.ajax
です。まず、setTimeout
について考えてみます。
alert('Hello');
setTimeout(function() {
alert('2秒経った');
}, 2000);
alert('GoodBye');
このコードを実行すると、以下のような結果になります。
- 「Hello」とアラートされる
- 「GoodBye」とアラートされる
- 2秒後に「2秒経った」とアラートされる
コードをそのまま素直に受け止めると、2と3は逆になりそうな気がしますが、実際には、「2秒経った」とアラートされるのは最後になります。
次はjQueryの$.ajax
の例です。
alert('Hello');
$.ajax({
url: 'something.txt',
success: function() {
alert('テキスト取ってきた');
}
})
alert('GoodBye');
これも先ほどの例と同じで、次のような実行結果になります。
- 「Hello」とアラートされる
- 「GoodBye」とアラートされる
- ajaxが完了したら「テキスト取ってきた」とアラートされる
このような結果になるのは、setTimeout
も$.ajax
も、非同期な処理だからです。
setTimeout
も$.ajax
も、実行されたタイミングと、その処理が完了するタイミングが異なります。処理が完了するのは、いくらかの時間が経ってからになります。setTimeout
の例でアラートが出るのは、2秒経ってから、$.ajax
の例でアラートが出るのは、something.txt
の取得が完了してからとなります。
ですが、アラート表示やテキスト取得の処理の完了を待つことなく、続けて書いたコードが実行されます。進行中のsetTimeout
、$.ajax
は、処理が完了した時点で、完了時に登録した関数が実行されます。非同期であるということは、このようなことをいいます。
これとは逆にalert
は、同期的に処理されます。alert
が出ると、ダイアログが表示され、そのダイアログを閉じるまで、続けて書かれたコードは実行されません。つまり処理が完了するまで待つわけです。
alert('Hello')
は処理が終わるまで、次のsetTimeout()
は実行されないが、setTimeout()
は処理の終わりを待たずに、次の処理alert('GoodByd')
が実行される。前者は同期的処理、後者は非同期的処理という。
このように、処理が終わるまで待つのが同期的な処理、処理が終わるまで待たないのが非同期な処理です。
コールバックを使う
このように完了を待たないのが非同期処理なわけですが、JavaScriptを書いていく上では、たくさんの非同期処理を扱わなければなりません。今挙げたajaxは基本、非同期で行いますし、jQueryでよくやる要素の幅や高さをアニメーションさせるのも、アニメーションが完了するのは、アニメーションを開始してからしばらく時間が経った時であり、その間にも続けて書いたコードが処理され続けるため、非同期な処理であるということができます。
このような非同期処理を行うときは、その処理が終わったときに、なにか処理を行わせたいことが多いです。
例えば$.ajax
であれば、当然、その内容がほしいから行うわけですし、setTimeout
も、時間が経った後になにかさせたいから使うわけでして。
このようなタイミングになにかさせる場合には、コールバックという仕組みを用います。例えば次のようなかたちです。なにかしらのWebサービスを想像してください。
// ユーザーIDをajaxで取得する関数
var getUserId = function(callback) {
$.ajax({
src: '/path/to/api',
dataType: 'json',
success: function(data) {
callback(data.userId);
}
});
}
// ユーザーIDを取得し、アラートする
getUserId(function(userId) {
alert('ユーザーIDは' + userId);
});
getUserId
は、ajaxでサーバーのAPIにアクセスし、自身のユーザーIDを取得するための関数であると想定します。この中で$.ajax
を使っています。ユーザーIDが得られるのは、ajax処理が完了した時であり、そしてこのajax処理自体が非同期な処理です。すぐに結果はわかりませんが、非同期な処理なので続けて書いたコードは実行されてしまいます。しかし、そうではなくて、そのユーザーIDを取得した時に続きの処理を行いたい。ではどうすればよいのか。
ここで使われるのが、コールバックという方法です。getUserId
関数に、完了時に実行したい関数を引数として渡し、処理完了時にこれを実行します。この例ではgetUserId
実行時に、引数として渡している無名関数が、その完了時に実行したい関数です。
getUserId
の中身を見てみると、$.ajax
完了時に、この受け取った関数を実行しています。これがコールバックという仕組みであり、このように渡される関数は、コールバック関数と呼ばれます。
さらにいうと、$.ajax
に渡しているオブジェクトのsuccess
に指定している関数も、コールバック関数です。ajax処理が完了したら、これを実行してくれという関数をsuccess
に指定しています。時間を置いて結果がわかるような処理に対しては、すぐに結果を受け取ることができません。これを解決するのが、コールバックという仕組みです。JavaScriptでなにか書くときは、こんな具合にコールバックを多用することがあります。
ちなみにコールバックという言葉は、元来、電話のかけ直しのことをいうそうです。「オレ電話代払いたくないんで、ワンギリするからかけ直して」というのがコールバックらしいです。
コールバックの困るところ
このように非同期処理をうまく扱うために必要なのがコールバックですが、これを多用すると、困ることがあります。それは関数の入れ子が深くなりすぎてしまうということです。
例えば、ユーザーの親子兄弟関係を扱うようなサーバー側の仕組みがあったとします。これを用い、ユーザーIDを元に、その父親の母親の兄弟の情報(つまり大叔父や大叔母)を取得するケースを考えてみます。
サーバー側には、ユーザーIDから父親のIDを返すAPI
、母親のIDを返すAPI
、兄弟のIDを返すAPI
の3つがあったとします。そんなとき、こんなコードを書くかもしれません。
getFatherId(userId, function(fatherId) {
getMotherId(fatherId, function(motherId) {
getBrothersIds(motherId, function(brothersIds) {
console.log(brothersIds); // 兄弟のID
});
});
});
父親のIDを取得し、その母親のIDを取得し、その兄弟のIDを取得します。これらはすべて非同期の処理なので、今解説したコールバックの仕組みを使っているわけですが、これらは、ひとつひとつ順番を守って実行する必要があります。その結果、上記のようなコードになります。このように、コールバックという仕組みは便利なのですが、複雑な仕組みになると、だんだん入れ子が深くなり、可読性も下がりますし、書くのも辛くなってきます。
これを解決してくれるのがdeferredです。今の例をdeferredを使って書くと、次のように書けそうです。
getFatherId(userId)
.then(getMotherId)
.then(getBrothersIds)
.then(function(brothersIds) {
console.log(brothersIds); // 兄弟のID
});
どちらがよいと、一概にいえるものではありませんが、deferredを使うと非同期な処理をシンプルに、統一したやり方で行うことができます。
前置きが長くなりましたが、そんなdeferredをサンプルを通して解説していきます。
doneとはなにか
1つめのサンプルでは、deferredの基本的な動作を確認してみます。
サンプル:01-done
codegrid-jQueryDeferred/01-done at gh-pages · pxgrid/codegrid-jQueryDeferred · GitHub
このサンプルを開き、ボタンをクリックすると、2秒後に「成功しました」とアラートが出ます。
var doSomething = function() {
var defer = $.Deferred();
setTimeout(function() {
defer.resolve(); // 解決
}, 2000);
return defer.promise(); // プロミスを作って返す
};
$(function() {
$('#button').on('click', function() {
var promise = doSomething();
promise.done(function() {
alert('成功しました');
});
});
});
現場の実装では、たったそれだけのことをするために、わざわざこのサンプルのコードのような書き方はしませんが、deferredの動作を確認するため、このコード内で何が行われているのかを見ていきます。
deferredを作り、そのpromiseを返す
このコードのキモとなるのは、doSomething
という関数です。まず、この中では、以下のようなコードがあります。
var defer = $.Deferred();
このように$.Deferred
を実行すると、deferredオブジェクト(以降、deferred)が作られます。これを介していろいろと処理の連携をとるのが、deferredのやり方です。ちなみにここはnew $.Deferred()
としても同じです。そして、ちょっとコードを飛ばしまして、この関数の最後return
している部分を見てみます。
return defer.promise(); // プロミスを作って返す
ここでは、今作ったdeferredのpromiseオブジェクト(以降、promise)を作り、返しています。
さて、いきなりdeferredとpromiseというものが出てきましたが、とりあえずこの段階では、詳しい説明は後回しにします。ここで作ったdeferredであるdefer
は、非同期処理そのものを体現したオブジェクトであると考えてください。そして、このdeferredから作ったpromiseは、deferredのやり取りを外部と行うためのオブジェクトです。
解決された時の処理を登録する
次に、このdoSomething
を実行した箇所を見てみます。ボタンを押した後に実行される内容です。
var promise = doSomething();
先ほどのdoSomething
は、promiseを返すのでした。ですので、変数promise
に入れておきます。このpromiseは次のように使います。
promise.done(function() {
alert('成功しました');
});
ここでdone
が登場しています。promiseのdone
メソッドを呼ぶと、deferredが「解決」された時に、実行する関数を登録することができます。この場合「成功しました」というアラートが出ます。しかし、すぐには出ません。アラートが出るのは、あくまでもdeferredが「解決」された時です。
この部分の処理をまとめると、こんな具合になります。
doSomething
を実行する- いつ解決されるのかしらないけど、終わったら...
- 「成功しました」って出して
解決する
では、その「解決」されるとはどういうことか、doSomething
の中に戻ってみます。
var doSomething = function() {
var defer = $.Deferred();
setTimeout(function() {
defer.resolve(); // 解決
}, 2000);
return defer.promise(); // プロミスを作って返す
};
2000ミリ秒後経ったあとに実行されているdefer.resolve()
というのが、その解決するという動作を行っているところです。これが実行されると、promiseを通じてdone
に登録された関数が実行されます。これがdeferredの基本的な使い方です。
deferred図解
とりあえずdeferredを使ってみましたが、今起こったことを、図と併せて見てみましょう。
1. deferredの作成
まずdoSomething
が呼ばれ、deferredを新しく作りました。そして、そのpromiseを作り、これが返されました。promiseはdeferredと密接に関係しているオブジェクトです。
deferredには、状態(state)というものがあります。最初は、"pending"
になっています。まだなにも起こっていないという状態です。
2. doneで関数を登録
そして、ここにdone
で「成功しました」と出るアラートを出す関数を登録しました。
3. 関数が登録される
すると、登録された関数をdeferredが持っている状態になります。まだ状態は"pending"
のままです。
4. 解決する
2秒たったので、deferredがresolve
されました。これで、このdeferredは解決された状態になります。
resolve
が利用できるのは、deferredだけです。promiseにはresolve
できません。promiseはdeferredの持っている機能のうち、状態を変化させる機能を使えなくしたオブジェクトであると考えておいてもらって問題ありません。
なぜわざわざpromiseを渡すのか
処理のメインとなる部分ではdeferredをいろいろと操作し、他の機能とやり取りする部分ではpromiseを渡すというのが、deferredを使った設計パターンです。もしdeferredをそのまま返してしまうと、その機能の外側で解決(resolve
)や却下(後述するreject
)をされてしまうかもしれないので、このようにします。
べつに、わざわざpromiseを作って返さずとも、promiseができることはdeferredにもすべてできます。実装上、promiseが持っているメソッドについては、deferredもまったく同じメソッドを持っているような状態になっています。
そういうわけなので、そのままdeferredを返してしまってもよいのですが、きっちり処理内容を切り分けるためにこのようにします。promiseは、deferredの状態が変わった時に行う処理を登録するためだけに使います。
なかば無理矢理ですが、promiseのことは「後で処理完了したら教えるから!」というdeferredの約束をオブジェクト化したものとでも思っておいてください。deferredとpromiseの関係は、親分と子分、社長と秘書とでも言いましょうか。抽象的な存在なので、ちょっと例えが難しいですね。
補足:deferredとpromiseでやっぱりもやもやする人へ
先ほどは例えを使ったりして、いろいろと説明しましたが、このdeferredとpromiseの関係は、言葉だけで理解するのは難しいものであると筆者は考えています。実際に自分でコードを書いてみると感じがつかめると思いますが、それ以上、どうなっているのか知りたければ、jQueryのコードを眺めてみるのがよいかと思います。ややこしいですが……。
5. 登録されていた関数が実行される
deferredがresolve
(解決)されたので、状態は"pending"
から"resolved"
に変化します。これと同時にdone
で登録された関数が実行されます。
これが先ほどのサンプルの中で行っていた動作の一連の流れです。
failとはなにか
2つめのサンプルでは、deferredのメソッド、fail
を解説します。先ほどの例では、resolve
で「解決」し、その時にdone
で登録した関数を実行させる例でした。一方、今度のサンプルはreject
で「却下」し、その時にfail
で登録した関数を実行させています。
サンプル:02-fail
codegrid-jQueryDeferred/02-fail at gh-pages · pxgrid/codegrid-jQueryDeferred · GitHub
このサンプルを開き、ボタンをクリックすると、2秒後に「失敗しました」とアラートが出ます。
var doSomething = function() {
var defer = $.Deferred();
setTimeout(function() {
defer.reject(); // 却下
}, 2000);
return defer.promise(); // プロミスを作って返す
};
$(function() {
$('#button').on('click', function() {
var promise = doSomething();
promise.done(function() {
alert('成功しました');
});
promise.fail(function() {
alert('失敗しました');
});
});
});
なぜ、「失敗しました」と出るのでしょう。前のサンプルと異なる箇所を見てみます。
却下された時の処理を登録する
まずはボタンがクリックされた時の処理です。
var promise = doSomething();
promise.done(function() {
alert('成功しました');
});
promise.fail(function() {
alert('失敗しました');
});
前のサンプルではdone
で関数を登録していましたが、今度はfail
メソッドにも関数を渡しています。deferredは「解決」されたときとは別に、「却下」されたときにも、関数を登録できるのです。
その「却下」されたときに実行する関数を登録するのが、fail
メソッドです。今回は「失敗しました」と出ますから、どこかでdeferredが却下されたということになります。
却下する
今度はdoSomething
の中身を見てみます。
var doSomething = function() {
var defer = $.Deferred();
setTimeout(function() {
defer.reject(); // 却下
}, 2000);
return defer.promise(); // プロミスを作って返す
};
先ほどresolve()
になっていた箇所がreject()
になっているのがわかります。このようにdeferredのreject
メソッドを呼ぶと、その処理を却下したことになります。deferredは、却下されると、fail
メソッドで登録した関数を実行します。
今回のサンプルでは、2秒経った時、かならずreject
されるようになっていますので、絶対失敗するという結果になります。ですが、実際のコードでは、なにか失敗する可能性のある処理に対して、成功時にはresolve
、失敗時にはreject
をするという使い方になります。これについては追って見ていきます。
rejectされたときの状態
ちなみにreject
したとき、deferredの状態は"rejected"
になります。このdeferredの3つの状態、"pending"
、"resolved"
、"rejected"
というのは、普段はたいして意識する必要がありませんが、deferred.state()
といった具合に、deferredのstate
メソッドを呼ぶと、この文字列が確認できます。
var defer = $.Deferred();
console.log(defer.state()); // "pending"
defer.resolve()
console.log(defer.state()); // "resolved"
var defer = $.Deferred();
console.log(defer.state()); // "pending"
defer.reject()
console.log(defer.state()); // "rejected"
promiseが呼べるメソッド
resolve
と同様、reject
が使えるのは、deferredのみで、promiseにはできません。promiseが使えるのは、done
、fail
と、次に紹介するthen
だけであると覚えておいて、基本的には問題ありません。
thenとはなにか
3つめのサンプルでは、deferredのメソッドthen
を解説します。
サンプル:03-then
codegrid-jQueryDeferred/03-then at gh-pages · pxgrid/codegrid-jQueryDeferred · GitHub
このサンプルの実行結果は、ひとつ前のサンプルと同じです。2秒後に「失敗しました」というアラートが出ます。
var doSomething = function() {
var defer = $.Deferred();
setTimeout(function() {
//defer.resolve(); // 解決
defer.reject(); // 却下
}, 2000);
return defer.promise();
};
$(function() {
$('#button').on('click', function() {
var promise = doSomething();
promise.then(function() {
alert('成功しました');
}, function() {
alert('失敗しました');
});
});
});
結果は同じですが、今回はdeferredのthen
メソッドを使いました。
まとめて登録する
then
については、そんなに解説することはありません。done
とfail
を同時に登録できるだけと考えてもらってほとんど問題ありません。then
を使っている箇所を見てみましょう。
var promise = doSomething();
promise.then(function() {
alert('成功しました');
}, function() {
alert('失敗しました');
});
then
に2つの関数を渡しているのがわかります。1つめの関数がdone
、2つめの関数がfail
に渡す関数です。2つめの関数を指定しなければ、done
で登録したのと同じなので、基本、常にthen
を使っておいて問題ないです。
補足:$.Deferredの初期化時の処理
$.Deferred
は、初期化時に行わせたい処理を引数として渡すこともできます。例えばこのサンプルのdoSomething
は、次のように書いても同じです。
var doSomething = function() {
return $.Deferred(function(defer) {
setTimeout(function() {
//defer.resolve(); // 解決
defer.reject(); // 却下
}, 2000);
}).promise();
};
ajaxとはなにか
4つめのサンプルでは、ajaxとdeferredについて解説します。
サンプル:04-ajax
codegrid-jQueryDeferred/04-ajax at gh-pages · pxgrid/codegrid-jQueryDeferred · GitHub
今回のサンプルではボタンを押すと、外部ファイルのmessage.txt
をajaxで取得し、その内容「Hello! NAME」をアラートしています。
message.txt
Hello! NAME
script.js
var getGreetingMessage = function() {
var jqXHR = $.ajax({
url: 'message.txt',
dataType: 'text'
});
return jqXHR.promise(); // プロミスを作って返す
};
$(function() {
$('#button').on('click', function() {
getGreetingMessage()
.then(function(message) {
alert(message);
}, function() {
alert('メッセージの表示に失敗しました');
});
});
});
このコード内でなにが行われているのかを見ていきます。
ajaxとdeferred
このサンプルのgetGreetingMessage
関数の中を見てみると、$.ajax
の結果からpromiseを作り、それを返しているのがわかります。これはdeferredなのでしょうか? 正確には異なりますが、おおむね、そのようなものだと認識しておいて問題ありません。
jQueryはバージョン1.5から、ajax周りの処理に、deferredを使うように変更されました。むしろ逆に、ajaxのような非同期処理をもっとうまく扱うために、deferredが実装されたと考えたほうが自然かもしれません。
$.ajax
の返り値は、jqXHRオブジェクトという、飛ばしたリクエストに関する情報が入っているオブジェクトです。このオブジェクトは、deferredの機能も持ち合わせています。このためpromise
を作り、今までと同様にthen
で、リクエストの完了時に処理をすることができているのです。
ちなみに、いままでは、promiseを受け取った時、変数promise
へ一度保存していました。ですが、たいていの場合、そのあとthen
なりdone
なりを繋げ、以降、使わないことが多いので、今回のサンプルでは、受け取ったpromiseに対し、そのままthen
を繋げて書いています。
まとめ
今回は、jQuery deferredの基本的な部分やメソッドと、その前提となる概念について説明してきました。次回は、これを実際にこのような仕組みを、どのように実装に使っていくかという点について解説します。