jQueryのDeferredとPromiseで応答性の良いアプリをー基本編

jQuery 1.6 が既にリリース されていますが、1.5 で追加された Deferred を勉強する意味で、Script Junkie から Creating Responsive Applications Using jQuery Deferred and Promises を翻訳してみましたので、共有したいと思います。著者の Julian Aubourg は、jQuery のコアチーム・メンバーとして、Ajax モジュールの書き換えと Deferred 導入を指揮した方だそうです。

記事の途中、各メソッドの説明部分は、jQuery ドキュメントの翻訳に差し替えていますので、リファレンスとして使ってもらうのも良いかと思います。

DeferredPromise は、概念や用語が少々分かりにくいところがあるかと思いますので、読んでくださった方の、少しでも理解の助けになれば幸いです 😉 。

まえがき

この記事ではまず、deferreds の概念を探ります。Dojo、MochiKit などの JavaScript ツールキットには既にありましたが、最近ようやく JavaScript の人気ライブラリー jQuery 導入された、重要な機能です。

deferreds は、(ajax 要求に対する応答のように) ノン・ブロッキングな要素を抽象化し、将来のある時点で応答を返す promise (約束) を作成するための手段を提供します。まずはこの 約束 の概念を少しだけ詳細に説明してみたいと思います。

deferreds は、完了までに時間がかかる高負荷な処理の実装方法と考える事が出来ます。それらは、ブロッキング関数に対する非同期な代替え手段であり、ある要求の完了を待つ間、結果が返るまでブロッキングし続けるのではなく、すぐに deferred な (延期された) オブジェクトが返される、という考え方をします。そうしてその deferred オブジェクトにコールバック関数を登録し、要求が実際に完了したときに、すぐにそれらを実行するのです。

Promisesの概念

promisesdeferreds に関する説明のいくつかは、アプリケーションの実装に関する仕様にフォーカスをしたものとなっています。この章では、deferreds がサポートされたどの JavaScript フレームワークにも当てはまる、promises の役割について、一般的な解説を行うことにします。

最も基本的な定義としての promise は、延期された (あるいは将来の) 結果という概念を実現するための、ソフトウェア工学上のモデルです。その背景となる考え方は、ブロッキングをするであろう関数を実行するのではなく、代わりにいつかは満たされる将来の値をとして、promise (約束) を返す、というものです。

jQuery が Deferred オブジェクトの設計として元にした Promises/A は、その概念を次のように定義しています。

  • promise は非同期に実行され、その結果をある瞬間で見た時に、「終わっているか・いないか」 を表す状態インターフェース・オブジェクトとしてデザインする
  • Promise の初期状態は unresolved で、次の2つのいずれかの状態に遷移する
    • resolved : 実行の結果、データが得られた
    • rejected : 実行の結果、問題が生じ、データが得られなかった
  • メッソド then により、状態遷移時のコールバック関数を登録し、将来に備える
promiseの状態遷移

promiseの状態遷移

具体例として、サードパーティーの API から入手するデータに非常に依存した Web アプリケーションの構築を考えてみます。ある時点における API サーバーの遅れ時間が不明なため、応答が返るまでアプリケーションの他の部分の実行がブロックされるという、この手のアプリケーションに共通の問題に直面します。promise は、ブロッキングの効果を無効化し、完全に分離するという方法で、この問題に良い解決方法を提供します。

promise を返すサードパーティ製の API を扱う仮想コードは、次の様になります。

promise = someRequestAPI( arg1, arg2, ...);
promise.then( function( futureValue ) {
	/* handle futureValue */
});
promise.then( function( futureValue ) {
	/* do something else */
});

ありがたいことに then メソッドは、2つの引数を受ける事が出来ます。1つは promiseresloved になった (成功した) 時のため、もう1つは、rejected になった (失敗した) 時のためのものです。仮想コードでは次のようになります。

promise.then(
	function( futureValue ) {
		/* we got a value */
	},
	function( futureValue ) {
		/* something went wrong */
	}
);

実際のアプリケーションでは、複数の結果を得る事が必要になるでしょう。例えば、ユーザーに興味のあるオプションを選択させる前に、オプションの集合を動的に表示する状況です。この場合には、when と呼ばれるメソッドが存在し、全ての promises が正常に満たされた場合のアクションを実行するために使う事が出来ます。

when(
	promise1,
	promise2,
	...
).then(function( futureValuePromise1, futureValuePromise2, ... ) {
	/* all promises have completed and are resolved */
});

さらに、アニメーションを複数同時に動かす時のシナリオが良い例題となります。それぞれのアニメーション動作が終了したかどうかをトラッキングし続けない限り、シナリオの完了をしるのは困難です。ですが、promises と メソッド when を使えば、それぞれのアニメーションが 「自分は終了します!」 という 約束 を効果的に伝える、とてもストレートな方法で実装できます。再び仮想コードで例題を見てみましょう。

when(
	function(){
		/* do animation 1 */
		/* return promise 1 */
	},
	function(){
		/* do animation 2 */
		/* return promise 2 */
	}
).then(
	function(){
		/* once both animations have completed
		   we can then run our additional logic */
	}
);

上記の例は、同期の機構を使わずに、ノン・ブロッキングなロジックの記述が出来るという事を示しています。

次の章では、jQuery における deferreds の実装方法について見てみます。今見て来た promises が驚くほど簡単に実装できる事が分かるはずです。

jQueryのDeferredオブジェクト

jQuery 1.5 で初めて導入された deferreds の実装は、前章で見て来た promises の概念とそれほど違いはありません。

基本的には、従来のライブラリー単体では出来なかった、将来のある時点で結果を返すまで、defer (遅延) させることが出来るようになります。

jQuery の Deferred オブジェクトは、「CommonJS の Promises/A」 のデザイン原則に基づき、Julian によって ajax モジュールの大幅な書き換えの一部として追加されました。以前のバージョンの jQuery では、jQuery.ajax() に、正常かエラーを処理するコールバック関数を渡しており、これは非常に結びつきの強いものでした。他の言語やツールキットも同様です。

jQuery の新しいバージョンでは、コールバック関数を管理する方法がいくつか拡張されていて、それは驚くほど柔軟です。jQuery のドキュメント は、Deferred オブジェクトで、何が出来るようになるのかを理解するのに役立つでしょう。

jQuery.Deferred()

Categories Deferred Object
Synopsis jQuery.Deferred( func )
Returns Deferred

Deferred オブジェクトのコンストラクタで、new 演算子を使う事も出来ます。オプションで、コンストラクタが戻る直前に実行する関数を指定出来ます。この関数の実行時の引数は、生成された Deferred オブジェクトと this オブジェクトで、その中では、例えばメソッド deferred.then() を用いて、コールバック関数を登録する事が出来ます。戻り値は、新たに生成された Deferred オブジェクトです。

Deferred オブジェクトの初期状態は、unresolved (未解決) です。deferred.then()deferred.done()deferred.fail() によってオブジェクトに追加されたコールバック関数は、状態が遷移した時のためにキューに入れられます。

deferred.resolve()deferred.resolveWith() の呼び出しにより、オブジェクトは resolved 状態に遷移し、登録されている doneCallbacks 関数群を即座に実行します。

deferred.reject() あるいは deferred.rejectWith() の呼び出しにより、オブジェクトは rejected 状態に遷移し、登録されている failCallbacks 関数群を即座に実行します。

一度、オブジェクトが resolvedrejected の状態になると、それ以上、状態は変化しません。既に resolved あるいは rejected 状態の Deferred オブジェクトにコールバック関数が追加されると、それらは即座に実行されます。

Deferred オブジェクトは独自のメソッドを持っていますが、jQuery オブジェクト同様、チェーンさせる事が出来ます。生成された Deferred オブジェクトを直接チェーンさせてこれらのメソッドを用いる事が出来ますし、またオブジェクトを変数に代入し、これらのメソッドを実行する事も出来ます。

jQuery.ajax()

Categories Ajax
Synopsis jQuery.ajax( url, [ settings ] )
Returns jqXHR

非同期な Ajax 要求を実行します。戻り値 jqXHR オブジェクトには、Promise インターフェースの全てのプロパティとメソッドが与えられています。

jQuery.when()

Categories Core
Synopsis jQuery.when( deferreds )
Returns Promise

1つもしくはそれ以上のオブジェクトに対して、コールバック関数を実行する方法を提供します。

ただ1つの Deferred オブジェクトが jQuery.when() に指定された場合、Promise オブジェクト (Deferred メソッドのサブセット) が返されます。この戻り値を使い、例えば deferred.then() といった Promise のメソッドで、コールバック関数を登録する事が出来ます。Deferred オブジェクトが resolved もしくは rejected の状態になった時、通常は、その Deferred オブジェクトの生成元のコードにより、適切なコールバック関数が実行されます。例えば、jQuery.ajax により返された jqXHR オブジェクトは Deferred オブジェクトの派生であり、次のように使われます。

$.when( $.ajax("test.aspx") ).then(function(args) {
	alert(args[1]); /* args is [ "success", statusText, jqXHR ] */
});

jQuery.when には通常、非同期イベントを扱う Deferred オブジェクトを指定しますが、プレーンな JavaScript オブジェクトが含まれている場合、それは resolved 状態の Deferred オブジェクトとして扱われ、doneCallbacks に登録されたコールバック関数が即座に実行されます。この場合、rejected 状態にはならないため、failCallbacks を設定しても決して実行される事はありません。

$.when( { testing: 123 } ).done(
	function(x){ alert(x.testing); } /* alerts "123" */
);

複数の deferreds オブジェクトが jQuery.when に渡された場合、これら全ての deferreds オブジェクトの状態をまとめて管理する、新たなマスター Deferred オブジェクトが生成され、Promise オブジェクトとして返されます。この時 when は、全ての deferreds オブジェクトが resolved 状態になると、マスターの Deferred オブジェクトを resolved 状態にしますし、deferreds オブジェクトのうち1つでも rejected 状態になると、マスターの Deferred オブジェクトを rejected 状態にします。

マスターの Deferred オブジェクトが resolved 状態になった時、全ての deferreds オブジェクトの resolved 時の値が (thendone で登録された) コールバック関数に引数としてが渡されます。例えば、複数の jQuery.ajax() リクエストが when に指定された場合、jqXHR オブジェクトの success 時のコールバック関数への引数が、when に与えた順番に渡されます。

複数の Deferred オブジェクトが指定され、そのうち1つが rejected となった場合は、jQuery.when は即座に、マスター Deferred オブジェクトの failCallbacks を起動します。この時、オブジェクトの幾つかは、まだ unresolved の状態であるかもしれない事に注意して下さい。例えば未完了の ajax リクエストをキャンセルするなどの追加的な処理が必要であれば、クロージャーの中に jqXHR オブジェクトへの参照を保持し、その状態を調べたり、キャンセルするなどします。

例題

2つの ajax リクエストが成功した後に、1つの関数を実行します (ajax リクエストが successerror となるケースについては、jQuery.ajax() のドキュメント を見て下さい)。

$.when($.ajax("/page1.php"), $.ajax("/page2.php")).done(function(a1, a2){
	/* a1 and a2 are arguments resolved for the
	   page1 and page2 ajax requests, respectively */
	var jqXHR = a1[2]; /* arguments are [ "success", statusText, jqXHR ] */
	if ( /Whip It/.test(jqXHR.responseText) ) {
		alert("First page has 'Whip It' somewhere.");
	}
});
例題

2つの ajax リクエストが成功した場合には、myFunc を実行し、一方がエラーであった場合には、myFailure を実行します。

$.when($.ajax("/page1.php"), $.ajax("/page2.php"))
	.then(myFunc, myFailure);

deferred.promise()

Categories Deferred Object
Synopsis deferred.promise( [ target ] )
Returns Promise

deferred.promise() メソッドは、非同期な関数に対して、その進行や状態を他のコードから干渉されないよう、そういったメソッドのない (保護された) インターフェースを返します。即ち、ハンドラを追加登録するメソッド (thendonefail) か、あるいは状態を調べるメソッド (isResolvedisRejected) だけを許可します。jQuery1.6 では alwayspipe といった Deferred のメソッドも許可する予定です。

target が与えられた場合、deferred.promise() は新しいオブジェクトを作る代わりに、そのオブジェクトに Promise のメソッドをアタッチし、そのオブジェクト自体を返します。これは、既に存在するオブジェクトに Promise インターフェースを与える場合に有効です。

もし、Deferred オブジェクトを作ろうとするなら、どこかで resolved あるいは rejected の状態にするために、その Deferred オブジェクトへの参照を保持するのが賢明です。deferred.promise() から返された Promise オブジェクトだけを返せば、他のコードが、コールバック関数を登録したり、現在の状態を調べたりする事が出来ます。

例題

Deferred オブジェクトを生成し、あるランダムな時間後にそのオブジェクトを resolvereject する2つの関数をタイマーにセットします。2つのうちどちらか先に起動された方がコールバック関数を実行します。後にタイムアウトを迎えた関数は、既に1つ目の関数によって Deferred が完了している (resolvedrejected の状態にある) ため、何の効果も与えません。

例題

既に存在するオブジェクトに Promise の振る舞いをさせるため、target を使う例です。

状態遷移時のコールバックを登録する関数群

deferred.then()

Categories Deferred Object
Synopsis deferred.then( doneCallbacks, failCallbacks )
Returns Deferred

両引数とも、1つの関数、あるいは関数の配列を指定する事が出来ますし、必要がなければ null を指定する事が出来ます。本メソッドの代わりに、doneCallbacks あるいは failCallbacks だけを指定するために .done() あるいは .fail() を使う事が出来ます。

Deferred オブジェクトが resolved になった場合は、doneCallbacks が実行されます。一方、rejected になった場合には、failCallbacks が実行されます。これらのコールバック関数は、追加された順に実行されます。deferred.thenDeferred オブジェクトを返すため、.then() を含め、引き続き他の Deferred メソッドをチェーンする事が出来ます。

例題

jQuery.get() メソッドは、Deferred オブジェクトから派生した jqXHR オブジェクトを返すため、.then メソッドを使い、ハンドラをアタッチする事が出来ます。

$.get("test.php").then(
	function(){ alert("$.get succeeded"); },
	function(){ alert("$.get failed!"); }
);

deferred.done()deferred.fail()

Categories Deferred Object
Synopsis deferred.done( doneCallbacks, [ doneCallbacks ] )
deferred.fail( failCallbacks, [ failCallbacks ] )
Returns Deferred

Deferred オブジェクトが resolved の状態になったときに実行させるコールバック関数を doneCallbacks に、rejected の状態になったときに実行させるコールバック関数を failCallbacks に指定します。

いずれのメソッドも、1つ以上の引数をとり、関数か、関数の配列が指定可能です。コールバック関数は、追加された順番に実行されます。各コールバック関数には、deferred.resolve()deferred.resolveWith()deferred.resolve()deferred.resolveWith() で指定された引数が渡されます。また両メソッドとも、Deferred オブジェクトを返すため、他のメソッドを続けてチェーンさせる事が出来ます。

例題

ボタンが押されたときに、Deferred オブジェクトを resolved の状態にし、複数のコールバック関数を起動させます。

例題

jQuery.get() メソッドは、Deferred オブジェクトから派生した jqXHR オブジェクトを返すため、deferred.done()deferred.fail() メソッドで、successerror 時のコールバック関数を登録する事が出来ます。

$.get("test.php")
	.done(function(){ alert("$.get succeeded"); })
	.fail(function(){ alert("$.get failed!"); });

Deferredオブジェクトの状態を遷移させて、コールバックを起動する関数群

deferred.resolve()deferred.resolveWith()deferred.reject()deferred.rejectWith()

Categories Deferred Object
Synopsis deferred.resolve( args )
deferred.resolveWith( context, [ args ] )
deferred.reject( args )
deferred.rejectWith( context, [ args ] )
Returns Deferred

deferred.resolve(args) および deferred.reject(args) は、それぞれ deferred.resolveWith(this, args) および deferred.rejectWith(this, args) と同等です。

これらのメソッドは、Deferred を生成した親だけが呼び出すべきで、通常は deferred.promise() が返す Promise オブジェクトへのアクセスに限定することで、他のコードが Deferred オブジェクトの状態を変えてしまうことを防止します。

指定のコンテキストにおいて、Deferred オブジェクトを resolve あるいは reject し、args で与えられる引数の配列をセットして、deferred.then()deferred.done あるいは deferred.fail() によって追加された doneCallbacks あるいは failCallbacks を、追加順に実行します。また、Deferred オブジェクトが一旦 resolved または rejected の状態なると、それ以降に追加されたコールバック関数は、追加と同時に即座に実行されます。

現在の状態を問い合わせる関数群

deferred.isResolved()deferred.isRejected()

Categories Deferred Object
Synopsis deferred.isResolved()
deferred.isRejected()
Returns Boolean

Deferred オブジェクトが resolved 状態、あるいは rejected 状態であるかどうかを調べます。それぞれ真の場合に、true を返します。この状態は、そのオブジェクトに対し doneCallbacks あるいは failCallbacks のコールバック関数が実行された (あるいはその途中) であることを意味します。

Deferred オブジェクトは、unresolvedresolvedrejected の3つの状態のうち1つをとります。これらのメソッドは、デバッグの際、 Deferred オブジェクトの状態を調べる時に、ことさら有効です。

アプリケーション例

さて、ここで実際に、3つの ajax リクエストを連携させる .when().then() の例を見てみます。

YSlow の接続パネルで観測したタイムライン・チャートは次の通りです。3つの ajax リクエストが並列に実行されているのが分かります。

3つのAjax要求

jQuery に備えられた $.when() の実装は、Deferred オブジェクトを解釈するだけではなく、Deferred ではない引数を引き渡す時でも、それらを Deferred であるかのように扱い、(doneCallbacks の) コールバック関数をすぐに実行するという点で、きわめて興味深いものです。加えて、deferred.then()Deferred のキューにコールバック関数を追加するために deferred.done()deferred.fail() メソッドの両方をサポートしているという点も注目です。

今度は、さらに数多くの Deferred の機能を使っている例を見てみましょう。(1) 外部のニュース・フィード、(2) $.get() (これは promise を返します) で最新のコメントを引き出すフィードという、最も基本的なアプリケーションを構築しようとしているものとします。このアプリケーションはまた、(3) ニュースとそのコメントの両方のアニメーションの準備をし、promise を戻り値とする関数 (prepareInterface()) を持っている、とします。

このアプリケーションに関連して追加の処理を実行する前に、これらすべての3つのステップを確実に実行するために、$.when() を使います。.then().fail() ハンドラは、必要に応じて追加のアプリケーションを実行するために使う事が出来ます。

後半へつづく…

さて、前半は 「基本編」 というタイトルをつけましたが、後半は、キャッシュを有効活用した非同期リクエストや、ワンタイム・イベントといったテクニッックを使い、より実践的な例を取り上げる事になります。

私に理解できるか、甚だ不安ではありますが、お楽しみに 😀 。

jQueryのDeferredとPromiseで応答性の良いアプリをー基本編」への7件のフィードバック

  1. 通りすがり

    仮想コードの例 の箇所 whenの引数はDeferredであるべきなので、 function(){}() とするべきでは?
    when(
    function(){
    /* do animation 1 */
    /* return promise 1 */
    }(), <— 実行が抜けてる
    function(){
    /* do animation 2 */
    /* return promise 2 */
    }() <— ここも抜けてる
    )

    返信

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

海外からの投稿は受け付けていません