JSDeferredで,面倒な非同期処理とサヨナラ

第3回 JSDeferredを用いたアプリケーション開発(その2)

この記事を読むのに必要な時間:およそ 0.5 分

geolocationによる現在位置表示

次に,geolocationを使って現在位置を表示します。

まずは,geolocationをクロスブラウザ化させて,より簡単に利用するためのgetCurrentLocationという関数のsnippetを作ります。

リスト5

function getCurrentLocation (callback) {
  var geo = navigator.geolocation || google.gears.factory.create('beta.geolocation');
  var id = geo.watchPosition(
    function (pos) {
      geo.clearWatch(id);
      callback(pos);
    },
    function (e) {
    },
    {
      enableHighAccuracy: true,
      maximumAge: 0,
      gearsLocationProviderUrls: []
    }
  );
}

ここではまだJSDeferredを使用しません。なぜなら,これを単体のsnippetとしてJSDeferredに依存しない形で使いたいと思うかもしれないからです。この段階ではプリミティブなcallbackをうけとる関数にしておきます。

この関数はgearsによるgeolocationの実装と,ブラウザネイティブによるgeolocationの実装両方に対応するためのいくつかの処理をしているのと,確実にGPSの位置情報を取得するためにwatchPositionを使っています。詳しい説明はしませんが,とりあえずこう記述しておく上手に動作します(そういう意味で,JSDeferredから独立したsnippetというわけです⁠⁠。

さて,これをDeferredを返す関数にしたいと思います。withCacheと同じように,自分でDeferredオブジェクトを作ってd.callを呼ぶ,という方法でももちろん良いのですが,JSDeferredはこういった⁠コールバックをとる関数をDeferredを返すように簡単に変換する関数⁠を持っているため,それを使うことにします。

Deferred.connectは既存のコールバックをとる非同期関数をDeferredを返す関数に変換する関数です。これにより,既存の非同期関数を簡単にDeferredの処理の中に組込むことができます。簡単な使いかたは以下のとおりです。

リスト6

var getLocation = Deferred.connect(getCurrentLocation);
getLocation().next(function (loc) {
});

簡単ですね。これで他のJSDeferredの関数にようにnext()で繋いでいくことができます。では,これをアプリケーションに組みこんでみましょう。

geolocationの情報取得には多少時間がかかるため,同じく時間がかかるWikipediaの情報取得と同時に実行させたいものです。察しの良いかたはお気づきかもしれませんが,ここで第1回にでてきたparallel関数を使うことにします。

parallelは複数の非同期処理を実行し,結果を集める関数です。このアプリケーションの場合は以下のように用います。

リスト7

parallel({
  wikipedia: CollectLatLongFromCategory(...) // 前回書いた関数
  mylocation: getLocation()
}).
next(function (result) {
  // どちらの実行も終わったあとに呼ばれる
  alert(result.mylocation.coords.latitude, result.mylocation.coords.longitude);
});

CollectLatLongFromCategoryは前回と今回で取り上げた,Wikipediaから情報を取得し,Google Mapにプロットする関数です。これはDeferredを返すようにしていました。

getLocationは今Deferred.connectで変換した関数です。

parallelは両方の結果が揃ってから,すなわちWikipediaから取得したデータを全てプロットしてgeolocationで位置が確定してから,そのあとの処理を実行します。あとはここで自分の位置をGoogle Mapsにプロットし,ズームレベルなどを調整すれば完成です。

リスト8

parallel({
  wikipedia: CollectLatLongFromCategory(...) // 前回書いた関数
  mylocation: getLocation()
}).
next(function (result) {
  var lat = result.mylocation.coords.latitude;
  var lng = result.mylocation.coords.longitude;

  var myPos  = new google.maps.LatLng(lat, lng);
  var marker = new google.maps.Marker({
    position : myPos,
    map      : map,
    icon     : markerImageMe,
    title    : 'あなた'
  });

  var nearest, nearestPos;
  for (var i = 0, len = markers.length; i < len; i++) {
    var pos = markers[i].getPosition();
    var dis = distance(pos, myPos);
    if (!nearestPos || dis < nearestPos) {
      nearestPos = dis;
      nearest    = pos;
    }
  }

  var bounds = new google.maps.LatLngBounds();
  bounds.extend(myPos);
  bounds.extend(nearest);

  map.fitBounds(bounds);
  map.setZoom(map.getZoom() - 1);
});

マーカーの初期化などは省略しましたが,だいたいこのようになります。

まとめ

前回と今回でJSDeferredを使った簡単なJavaScriptアプリケーションを作ってみました。もしよろしければ,この例をJSDeferredを使わずに記述してみてください。おそらく,めんどうくさくて途中で投げだすか,JSDeferredのようなものを作りたくなるはずです。

煩雑な非同期処理が多くなりがちなJavaScriptでは,ある程度以上のアプリケーションを作ろうと思ったとき,JSDeferredや,あるいは何かしら似たようなものが必要になります。

次回はJSDeferredによるパフォーマンスの最適化と,その他応用方法にをご紹介したいと思います。

完成サンプルに追加されている機能

冒頭に示した完成版のものには,ここで説明した以外に,クロスブラウザのため以下のような機能が追加されています。

  • AndroidではlocalStorageがないため,代替としてGoogle GearsのSQLiteを使っている
  • WebKitにはunevalが実装されていないため,独自に実装している
  • ロード後に表示する範囲が広がりすぎないようにする
  • マーカーをクリックしたときにWikipediaの説明をポップアップさせる
  • geolocation中にキャンセルボタンを表示する

これらはJSDeferredとは直接関係ないため説明を省略しています。

著者プロフィール

cho45(さとう)

はてなエンジニア。サブテカ。バックエンドからインターフェイスまで,Perl,Ruby,JavaScript,ActionScriptなどを使いつつ Scala,Ioなどを触る,言語・コード表現ヲタク。

URLhttp://www.lowreal.net/
技術ネタhttp://subtech.g.hatena.ne.jp/cho45/