jQuery.Deferred を使って非同期処理をエレガントに書く #html5j

この記事は HTML5 Advent Calendar 2013 の 18日目の記事です。

HTML5 では様々な API が追加されました。そしていくつかの API は、コールバック関数を渡して、結果をその関数の呼び出しによって受け取る、「非同期」の API となっています。たとえば、XMLHttpRequestGeolocation APIFile APIIndexed Database API などが該当します(これらが HTML5 仕様かどうか自信無いですが…)。

これにより、実行時間のかかる処理であっても、JavaScript をブロックせずに実行できるのですが、非同期の関数を連続的に呼び出す場合、コールバック関数が深くネストしてしまい、見通しの悪いコードとなってしまいます。

これを解決する仕組みはいくつかありますが、今回は、jQuery の Deferred という仕組みを使って、連続した非同期処理をエレガントに書く方法を紹介したいと思います。

Deferred とは?

Deferred は特定のライブラリを指すものではなく、デザインパターンの一種です。様々な言語で実装されていて、JavaScript においても、今回紹介する jQuery に限らず、様々なライブラリやフレームワークで取り入れられています。

今回この記事では、Deferred の仕組みについては説明しません。詳しく知りたい方は、下記の記事が参考になるかと思います。

位置情報を取得して住所を問い合わせるサンプル

今回は、Geolocation API を使って位置情報を取得して、取得した緯度経度から Ajax を使ってサーバから住所を取得して表示する、というサンプルで説明します。

位置情報の取得にはユーザーの同意とデバイスからの情報取得が必要であるため、非同期の API となっています。また Ajax はサーバからの応答を待つ必要があるため、こちらも非同期です。これら非同期の処理を順番に処理するためには、コールバック関数の中で次の処理を呼び出す必要があり、その数が多くなるとネストが深くなってしまいます(31行目〜50行目)。

ネストが深くなるとコードの可読性が悪くなるのももちろんですが、各処理の依存関係が強くなり、コードのメンテナンスがしづらくなります。

Deferred を使ってエレガントに書く

そこで、Deferred の出番です。同じ処理を jQuery.Deferred を使って書いたものが下記になります(43行目〜64行目)。

一見あまり変わらないように思えますが、Deferred を使った場合は、コールバック処理が2つのパートに分かれているのがわかるかと思います(44行目~52行目と、54行目から63行目)。

もし、たくさんの非同期処理を順番に実行する場合でもネストが深くならず、このように順番に処理を記述することができます。特に、処理の順序を入れ替えたり挿入する場合に、コードの書き換えが最小限で済むことがイメージしていただけるかと思います。

Deferred の使い方

Deferred を使うためには、

  1. Promise を返す関数を作る
  2. .then() でつなぐ

が必要です。

1. Promise を返す関数を作る

Deferred を使って処理を行う場合、機能単位で関数にしてそれをつなぎ合わせるように組み立てていくと、エレガントに記述することが出来ます。このとき、その関数は必ず Deferred のルールにそって実装されている必要があります。

上記2つ目のソースの7行目、getCurrentPosition という関数は、そのルールに従っています。簡単に書くと下記のような構造になっています。

最初、// 1 で Deferred オブジェクトをインスタンスしています。その後、非同期の関数(asynchronousFunction)を呼び出していますが、非同期なので、引数で渡したコールバック関数が呼ばれる前に次の処理に移ってしまいます。

最後、// 3deferred.promise() を呼び出して Promise オブジェクトを取得し、return で返しています。

また、// 2 のコールバック関数内で、deferred.resolve(value) を呼び出していますが、これによって、次の処理に value を渡します。

基本的には、この3つを実装することで Deferred にルールに準拠した関数となります。

2. .then() でつなぐ

Deferred のルールに則った関数は、実行すると Promise オブジェクトを返しました。その Promise オブジェクトは、コールバック関数の中(// 2)の deferred.resolve(value) が呼ばれると、.then(function) を呼び出し、.resolve(value) で与えられた値を関数の引数として渡します。

上記コードの // 4 がその部分にあたります。非同期処理が完了した時に実行する処理を、.then() の第1引数に渡す関数内で記述すれば良いことになります。

また、この関数で Promise オブジェクトを返すと、また .then() が呼び出されます。これにより、.then() のメソッドチェーンの形で、非同期処理を1つずつ順番に実行していくことが出来ます。

ちなみにサンプルでは Ajax を使ってサーバから住所を取得しています。その関数(getAddress)の中には Deferred に関する記述がありませんが、実は、$.ajax() は Promise オブジェクトを返すようになっているので、そのまま利用することが出来ます。

なお、.resolve().reject() に置き換えると、エラーとして処理され、.then() の第2引数に指定された関数が実行されます。

すべての並列処理が完了したことを検知する

Deferred を使うことで、非同期の処理をステップバイステップで実行することが出来るようになりましたが、実はこれだけではなくもっと便利な使い方もあります。

たとえば、Ajax を使って複数の API を呼び出し、すべてのデータを合成して表示するような場合を考えます。Ajax は処理にかかる時間がまちまちで、複数のリクエストを行った場合にレスポンスの順番はランダムになります。そうすると、すべてのリクエストが完了したことを調べる処理は、多少複雑になってしまいます。

これを簡単に実行する仕組みとして、jQuery.when() が提供されています。この.when() の引数に Promise オブジェクトを列挙して渡してあげると、すべての処理が正常に完了した(deferred.resolve() が呼ばれた)時に、続く .then() を実行します。下記のようなイメージです。

この $.when() も Promise を返すため、.then() のメソッドチェーンを構築することができ、複数の並列処理を順番に実行することが簡単に出来ます。

まとめ

HTML5 では、さまざまな非同期呼び出しの API が提供されていて、コードが複雑になりがちです。非同期の処理を Deferred のルールに準拠した関数にしておくことで、それらを順番に実行したり、並列に実行したりすることが、シンプルなコードで書けるようになります。また、処理順序を変えたり、同時に実行する処理の数を調整する場合にも、少ない修正で対応でき非常に便利です。

今回は jQuery を使いましたが、アプリケーションフレームワークが標準で同様の機構を提供していたりします(AngularJS の $q とか)。複雑な HTML5 アプリケーションを開発する場合には必須のテクニックではないかと思いますので、ぜひお試しください。

参考リンク

住所取得の API は、農研機構の簡易逆ジオコーディングサービスを使用しました。

コメントをどうぞ