前回 は、「Creating Responsive Applications Using jQuery Deferred and Promises」 の記事と jQuery のリファレンスを翻訳し、Deferred の解説をお届けしました。が、今一歩、Deferred オブジェクトの うれしさ
をお伝えできていなかったように思います。今回はその反省をふまえ、単なる翻訳ではなく、元記事の文脈に沿って、Deferred オブジェクトをどのように使うと/何がうれしくて/どうしあわせになるのか、私が学習して得た事をお伝えしたいと思います。
今回は、4本の jsfiddle を記事に埋め込んでいます。jsfiddle 上で自由に編集してテストしてもらえばうれしいのですが、4本すべて実行すると1.4MBものリソースを読み込む事になり、ブラウザに優しくありません。すぐに
タブや
をクリックせずに、
タブで別窓を開き、テストが終わったら窓を閉じてから次の例題に移ると良いかと思います
。
Deferredのうれしさ
非同期なタイマー処理
5秒後にメッセージを表示するスクリプトは、大抵次のように書きますよネ。
setTimeout(function() {
alert("Hello from the future!");
}, 5000);
同じ事を Deferred を使って書くと、例えば次のようになります。
$.timeout = function(time) {
return $.Deferred(function(dfd) {
setTimeout(dfd.resolve, time);
}).promise();
}
$.timeout(5000).then(function() {
alert("Hello from the future!");
});
2行目のコンストラクタで Deferred オブジェクトを生成し、初期化処理として setTimeout() を設定しています。タイムアウトで起動される関数には dfd.resolve を指定していますが、この段階での実体はまだ空です。4行目で生成した Deferred を Promise 化し、外部からは then()、done()、fail() といったメソッド以外、Deferred オブジェクトを操作出来ないようにしています。7行目の then() でようやく dfd.resolve()、dfd.reject() で起動されるコールバック関数を登録する事になります。
さて、直接 setTimeout() を使えば3行で済むものを、こんなに面倒臭いコードを書いて、何がうれしいのでしょう?
実は Deferred の醍醐味の1つは7〜9行目にあります。この3行を素直に読むと、「5秒たったら、未来からのメッセージを表示する」 となりますよネ。なんかすごく直感的で、意味がストレートに伝わると思いませんか?
setTimeout(function() {
alert("Hello from the future!");
}, 5000);
$.timeout(5000).then(function() {
alert("Hello from the future!");
});
あらためて2つのパターンを並べてみると、最初の3行は、日本人が英語を翻訳するみたいに、単語を並べ替えなければ意味が通じません。それに比べ、最後の3行は可読性が高まり、美しいコード (?!) となっています。
え、それだけ?
う〜ん、あんまりうれしくありませんかネ。それでは次の例はどうでしょう?
Ajaxな処理
次のコードは、twitter の検索結果を表示します。
Twitter = {
search:function(query) {
var dfr = $.Deferred();
$.ajax({
url:"http://search.twitter.com/search.json",
data:{q:query},
dataType:'jsonp',
success:dfr.resolve,
});
return dfr.promise();
}
}
Twitter.search('jquery deferred').then(function(data) {
alert(data.results[0].text);
});
わざわざ Deferred と resolve() を介さなくても、$.ajax() の success: に直接、表示用のコード alert() を書けばもっと短くなるでしょう?
実はコレ、非同期な処理をする部分とその結果をインターフェースする部分が、ちゃんと分離されていて、コードの可読性が高いというメリットがあります。関数の中に関数が重なるより、それぞれが並列に並んでいた方が、読み易いですよネ。
さらに言えば、$.ajax() の success: に dfr.resolve() を指定した段階では、その実体は空です。後から then() によって、表示用のコールバック関数を登録しています。このように Deferred オブジェクトは、(unresolved から resolved に) 状態遷移してしまった後でコールバック関数を登録しても、ちゃんと処理してくれます。ページの読み込み状態によっては、ハンドラの登録が間に合わなくてイベントを逃してしまった、なんて言う事がなくなりますし、後からユーザーのインタラクションに応じてコールバック関数を変える、追加するなんていう事も可能です。これは Deferred オブジェクトの resolve() を介することによって生じるうれしさです。
ここまでのうれしさをもう1度まとめます。
- 非同期な処理をする部分と、その結果をインターフェースする部分を分離する事によって、(特にアプリケーションに近い部分で) コードの可読性が高まる
- より柔軟、かつ汎用的なコードが書ける
もう少し使える twitter 検索の例題を載せますので、色々いじってみて下さい。アプリケーション層に近い search() と show() の階層が深くならず、同列で書けている事が分かると思います。
参考情報
Deferredが有効なパターン
前回に引き続き、「Creating Responsive Applications Using jQuery Deferred and Promises」 から、例題を紹介します。Ajax への適用が有効なパターンの典型例ですが、記事では他にも非同期な振る舞いをする処理への適用が紹介されています。これをちゃんと理解するために、Deferred オブジェクトの特徴をもう1度まとめておきます。
Deferredオブジェクトの状態遷移は、1方向かつ1度きりである
- 初期状態
unresolvedから、メソッドresolve()、reject()で、それぞれresolved、rejectedへの遷移が出来ますが、それ以上の変化は出来ません。- 複数の
Deferredオブジェクトをまとめて監視出来る
$.when()を使うと、Deferredオブジェクトの状態がまとめて監視が出来ます。Deferredオブジェクトを扱うメソッドは、チェーンさせる事が出来る
- jQuery オブジェクトがチェーン可能なのと同様、
$.when()、then()、done()、fail()、pipe()といったメソッドは、Deferredオブジェクト、あるいはその派生であるPromiseオブジェクトを返すため、メソッドを次々にチェーンさせる事が出来ます。
上記を押さえた上で、例題を見ていきましょう。
外部リソースのキャッシング
一般にブラウザは、画像やスクリプトなどの外部リソースを非同期かつ並列に読み込む事が出来ますが、一旦読み込んだリソースは再利用したいものです。ブラウザが保持しているキャッシュに関して言えば、(例えば 304 Not Modified レスポンスが返ってくるなど) サーバーとのネゴシエーションがうまく行けば、そのキャッシュを有効活用できますが、それでも HTTP リクエストだけは発行される場合があります。
ページが遷移しない Ajax アプリケーションなら、いっそ JavaScript でデータをキャッシュし再利用すれば、HTTP リクエストの再発行そのものを減らす事が出来ると考えるのは自然でしょう。そこで、「一度読み込んだら、次からは HTTP リクエストは出さない」 という振る舞いを、Deferred オブジェクトの一方向かつ一度きりの状態遷移で実現させます。
まずは、キャッシュを管理する汎用的な関数を返すコードです。クロージャー中の連想配列 cache に Deferreed オブジェクトを保持させています。
$.createCache = function( requestFunction ) {
var cache = {};
return function( key, callback ) {
if ( !cache[ key ] ) {
cache[ key ] = $.Deferred(function( defer ) {
requestFunction( defer, key );
}).promise();
}
return cache[ key ].done( callback );
};
}
元記事では、この汎用キャッシュ管理関数を使い、次の4つの機能を例題として取り上げています。
- スクリプトの読み込み
-
$.loadScript = $.createCache(function(defer, url) {...}
-
- 画像の読み込み
-
$.loadImage = $.createCache(function(defer, url) {...}
-
- Ajax リクエスト
-
$.searchTwitter = $.createCache(function(defer, query) {...}
-
- タイムアウトの生成
-
$.afterDOMReady = $.createCache(function(defer, delay) {...}
-
1 と 2 は、ブラウザが十分に賢いため、HTML5 のローカル・ストレージ でも使わないと効果が見えにくいです (というか、サーバーのキャッシュ制御と遭わせて色々試したのですが、良い例題が出来ませんでした)。また 4 は setTimeout() で十分代替えが可能です。よって、ここでは 3 の例を紹介したいと思います (この章が、本記事のタイトル 「応答性の良いアプリ」 のハイライト部分なのですが、紹介数が少なくて済みません
)。
Ajaxデータのキャッシング
コードの構造としては、前章で取り上げた Twitter 検索に、先の汎用キャッシュ管理関数を組み込んだものとなっています。同じ検索語であれば、HTTP リクエストは再発行されません。検索の履歴機能なんかも出来そうですネ。
複数アニメーションの同期
元記事は jQuery 1.5 を前提に書かれており、当時はまだ Deferred オブジェクト (あるいはそのサブセットである Promise) と結びつけることが出来るオブジェクトは jqXHR だけでした。一方、jQuery 1.6 からは任意のオブジェクトに結びつけることが出来ます。これが意味するのは、任意のオブジェクトの状態遷移を観測することが出来る
ということです。オブジェクトに Deferred を割り当て、そのインターフェースである Promise を得るメソッドが .promise() です。
例えば、<div> 要素に対しては、$('<div/>').promise() で Promise を得る事が出来ます。これは、HTML が静的な状態では、単に resolved 状態を示すだけですが、アニメーションを適用すると、その終了状態を観測することが出来ます。次の例は、複数の要素のアニメーション完了を観測します。
ワンタイム・イベント
あるイベントに対して、1回だけ何らかの初期化処理を行いたい、という場合があるかと思います。通常はフラグを使って、次のように実装するでしょう。
var buttonClicked = false;
$( "#myButton" ).click(function() {
if ( !buttonClicked ) {
buttonClicked = true;
initializeData();
showPanel();
}
});
if ( buttonClicked ) {
/* perform specific action */
}
この実装のネガは、イベントが増えるごとに、フラグの制御と判断処理が必要になるという事です。これを次のようにスマートに書きたいとは思いませんか?
var myEvent = $("#myButton").firstClick();
myEvent.done(initializeData);
myEvent.done(showPanel);
myEvent.done(function() {
/* perform specific action */
});
関数 bindOnce() は次の3つの事を行っています。
- 与えられた要素に対し、指定イベントの
Deferredオブジェクトが既に割り当てられているかをチェックする - もしなければ
Deferredオブジェクトを作成し、イベントが発生したときに 1回だけresolve()を実行する関数を定義する Deferredオブジェクトにコールバック関数を登録し、Promiseを返す
まとめ
今回は、どのような視点で記事をまとめるか、結構悩みました。「これが典型的な Deferred のパターンだ!」 というのがあれば良いのですが、なかなかそうは行かず、多彩な書き方が出来てしまいます。そこで、押さえるべきポイントを中心にまとめたつもりです。
jQuery は今までどちらかというと、見た目の装飾に使われる例が多かったように思います。が、Deferred のサポートで、使い易い範囲が益々拡大するのではないでしょうか。
この記事が、Deferred 活用の一助になれば幸いです。ご意見、ご指摘がありましたら、お願いします。
ピンバック: 10月16日の注目記事 | Javable.Jp