投稿日:

jQuery.Deferred をもう少し理解する


jQuery.Deferred の使い方を示した記事はたくさんありますので、ここではなるべく原理にフォーカスして改めて説明してみます。なお、この記事は既に jQuery.Deferred の利用方法をある程度理解されている方に向けての記事になります。

§ 基本と課題

jQuery.Deferred は、実は、登録したコールバックを resolve() や reject() のタイミングで実行するだけの(基本は)シンプルな API です。次の例は単に「Success」をアラートします。

dfd.resolve() ではなく、dfd.reject() とすると、Deferred.fail() に設定した関数が呼ばれます。Deferred オブジェクトは、基本的には、ただそれだけのシンプルな機能です。

しかし jQuery.Deferred は上の例のような使い方は通常しません。一般に、Deferred は非同期処理(Ajax通信やsetTimeoutなど)のフロー制御の為に利用され、またそのために、直感的に扱えるようなメソッドの名前が付けられています。ただ、実際の利用例を見ると、それはやや魔法のように見えなくもなく、原理的な理解が少し難しくなるような気がします。

そこで抑えたいのが、jQuery.Deferred は「基本的に単純にコールバックを実行するだけの仕組み」だと理解しておくことです。この理解があると、よくある利用例を見てもその意味を正しく理解して、より応用的・発展的な利用ができるようになると思います。

§ 非同期処理での利用

では、一般的な利用方法を見てみましょう。ここでは、非同期処理の実装として、setTimeout を利用します。

このスクリプトは、「start」とアラートしてから2秒待って、「end」「We love KARA!!」と、合計3回アラートが表示されます。

ここでのポイントは、[1] のセクションで任意の処理を提供していることと、[2] のセクションでその結果を受けて別の処理を行っていることです。jQuery.Deferred が備えるシンプルなコールバックの仕組みを、最初の例とは順序を逆にして使うことで、「ある処理の提供」と「その結果処理」というフェーズの異なる2つのタスクを上手く分離できていることです。

ここで、あくまでもシンプルなコールバックの仕組みが利用されているに過ぎないことを理解してください。

次に、jQuery.ajax での例を見てみます。

この例は、ajax 通信が成功して終わった後、その結果を alert します。
ajax による非同期通信が終わったら、その結果を受けて done() に設定したコールバックが呼ばれています。この例において、jQuery.ajax のコールは、サンプルコード S02 の「[1] 任意の機能を提供する」と同じ役割であることが理解できます。

この理解を深める為に、S02 のコードの [1] のセクションを次のように書き換えてみました。

そしてこれは、jQuery.ajax と同様に次のように利用できます。

このことから jQuery.ajax でも、内部ではこれと同じようなことが行われ、非同期通信の結果が得られたタイミングで内部的に Deferred.resolve() されているに過ぎないと理解できると思います。この仕組みを正しく理解していると、似たような機能を持った関数やオブジェクトを、自分で作成することも簡単に出来る筈です。

ところで、jQuery.ajax が返すのは jQuery.Deferred オブジェクトではなく、jqXHR というオブジェクトです(http://api.jquery.com/jQuery.ajax/)が、このjqXHR オブジェクトは、jQuery.Deferred オブジェクトのサブセットとなる Promise インターフェースを実装してるため、jQuery.Deferred オブジェクトと同様に done() などが利用できます。

§ Promise インターフェイス

Promise インターフェイスは、jQuery.Deferred オブジェクトのサブセットです。

ここまでに見て来たように、jQuery.Deferred オブジェクトはコールバックの登録(doneやfail)と呼び出し(resolveやreject)という2つの役割が利用する機能を、1つのオブジェクトで提供していますが、jQuery.ajax の例のように、結果だけを利用する(したい/させたい)場合も多くあります。そこで、Deferred から結果の利用側のメソッドのみを抽出した、Promise インターフェースがあります(http://api.jquery.com/Types/#Promise)。

Promise インターフェースでは、done() や fail() といった、処理結果に関わるメソッドのみが公開され、resolve() や reject() といった、機能の提供側のメソッドを含みません。このため、外部から予期せず勝手に resolve されるリスクも回避できます。

そして、jQuery.ajax が返す jqXHR オブジェクトもこの Promise インターフェースを実装しているため、AJAX 通信の完了を done() や fail() によってハンドリングできるわけです。

なお、jQuery.Deferred オブジェクトもまた、promise() メソッドを実装しています。Deferred の promise() メソッドは単に、オリジナルの Deferred の単純なサブセットとしての Promise オブジェクトを返します。

§ jQuery アニメーションでの利用

jQuery で利用できるアニメーションでも、Promise インターフェスを使った事後処理が利用できます。例えば次のサンプルを見てみましょう。

このコードは、#object1 を取得し、2秒掛けてフェードアウトさせ、フェードアウト完了後に「Finish」とアラートします。

ここで jQuery.promise() は Promise オブジェクトを返すメソッドです(※1)。jQuery.ajax() が返す Promise オブジェクトは非同期通信が終了するまで待って、resolve() するものでした。一方、jQuery.promise() が返す Promise オブジェクトは、その jQuery オブジェクトのアニメーションキューにある全てのアニメーションが終了した時に resolve() します。結果、S05 のコードはアニメーション終了後に alert() します。

(※1 ただの jQuery オブジェクトにもこんな機能があったんですね。この機能は、昨日 @woodroots さんに教えていただくまで知りませんでした)

§ Promise.then

最後に、Promise インターフェースの then について少し解説します。

done() や fail() と異なって、then() はその後の then() や done() などに処理を繋げる機能があります。連鎖的に次々と処理を実行していきたい場合に役立つ機能です。

次の then() を使った例を見てください。

それぞれの then() の中で、値を return しているところと、そうで無いところがあるところに注目してください。この違いは何でしょうか?

本来 jQuery.Deferred が提供する機能の目的は、非同期処理のフロー制御です。そして then() は、非同期処理のフローをシリアライズ(直列化)することが目的で、1つの処理が終わってから次の then() や done() に処理を流したいわけです。そのために、どこかの then に非同期処理が含まれるなら、その完了を検知するために Promise オブジェクトが必要になります。

そこで、then() はそのコールバック関数の戻り値として「Promise オブジェクトを返す promise() という関数を持った任意のオブジェクト」を期待します。…と、言葉にすると少し難しそうですが、要するに jQuery.Deferred オブジェクトそのものや、jQuery オブジェクトも promise() メソッドを持っているのでこれに該当します。これらのオブジェクトが then に与えられた関数から return された時、then はそのオブジェクトの promise() メソッドをコールして Promise オブジェクトを取得し、その Promise オブジェクトから処理の完了を検知して、次の then() や done() に処理を渡します。このような仕組みによって、then() は幾つかの非同期処理をシリアライズします。

言い換えると、「then() の中の処理が非同期な場合には、その完了をもって resolve() する Promise オブジェクトを返す promise() メソッドを持ったオブジェクトを返す必要がある」となります(やっぱり文字にするとややこしいですね ^ ^;)。

なお、then() に与えられた関数が promise() メソッドを持ったオブジェクトを返さない時、then は即座に次の then() や done() に処理を移します。つまり、何も待ちません。

 

以上です。質問などがあれば回答します。間違いがあればご指摘ください。

また、この記事の作成にあたって @woodroots さんとのやりとりが大変参考になりました。ありがとうございました。