モーグルとカバとパウダーの日記

モーグルやカバ(EXカービング)山スキー(BC)などがメインの日記でした。今は仕事のコンピュータ系のネタが主になっています。以前はスパム対策関連が多かったのですが最近はディープラーニング関連が多めです。

javascriptでdeferredを使ってコールバックをきれいに書く

最近、ちょっと作りたいWebサービスがあって、不慣れなjavascriptを書いてるのですが、上手い書き方がわからずに困っていたものがありました。


それは、Web APIからのコールバックをどう書くか、という問題です。


通常、javascriptでWeb APIを呼ぶ場合は、JSONPを使ってコールバック関数を呼び出す形にします。
具体的に例を挙げて説明しましょう。
Ustのチャンネル名からその動画を貼り付ける場合、getInfoなどのWeb APIを呼び出して、その中のembedTagを貼りつけてやります。

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>

<div id="ust"></div>
function getUstInfo( channel ) {
  var api_url = 'http://api.ustream.tv/json/channel/';
  var api = api_url + encodeURIComponent( channel ) + '/getInfo?callback=?';
  $.getJSON( api, function( info, status ) {
    displayUst( info.embedTag, '#ust' );
  });
}

function displayUst( embed_tag, target ) {
  $( target ).replaceWith( embed_tag );
}

getUstInfo( 'ぬこのこ' );


この書き方にはいくつか問題があります。
getUstInfoでは本来、Ustの該当チャンネルのデータをもらってくるだけをしたいのですが、これだけで表示までが行われてしまいます。
実は、データだけを持ってくるような関数を書くことが出来ないので、仕方なくその後の表示まで一緒にやってしまっているのです。
だから、もし他のところでもinfoを取ってきたい場合には、同じコードをコピペで書かないといけません…
この問題は、コールバックが1つだけであればまだ良いのですが、複数のWeb APIを呼んでその結果をまとめて表示に利用する場合には、コールバック関数からさらにコールバックを呼び出して… というふうにコーディングしなければならず、だいぶ汚い書き方になります。
また、Web API呼び出しが並列に行えないため速度的にも不利です。


コールバック関数の中で値返せばいいじゃん、と考えて下記のように変更すると、上手く動きません。

var ust_tag;

function getUstInfo( channel ) {
  var api_url = 'http://api.ustream.tv/json/channel/';
  var api = api_url + encodeURIComponent( channel ) + '/getInfo?callback=?';
  $.getJSON( api, function( info, status ) {
    ust_tag = info.embedTag;
  });
}

function displayUst( embed_tag, target ) {
  $( target ).replaceWith( embed_tag );
}

getUstInfo( 'ぬこのこ' );
displayUst( ust_tag, '#ust' );

こう書いてあるとコールバック関数はその場で実行されるような気がしてしまいますが、実際にはWeb APIが呼ばれていますから、その答えが返ってきたタイミングでgetJSONで渡されている中身(ust_tag = info.embedTag)が実行されます。
だから、displayUstが呼ばれるタイミングではまだ情報が返ってきていないため、表示がされません。


こういった場合どうしたらいいだろう… とtweetしてたら、@KojiSaitoさんと@hkobaさんからいろいろと助言をいただき、@hkobaさんから「Deferred使えばいいよ」と教えていただきました。


Deferrdとはなにかというと、詳しくはこちらの
jQueryのDeferredとPromiseで応答性の良いアプリをー基本編 | ゆっくりと…
の説明が超おすすめなのでそれを読んでもらうとして、ひとことで言うと「準備が出来た時点でこれやってね!というのを書ける仕組み」なのです。


Deferredの基本的な使い方は、promiseで一旦返しておいて、実際に処理が終わった時にはresolveというのを返します。そしてresolveした時に実行する内容はthenというメソッド内に書いておきます。
実際に書いたものを見たほうがわかりやすいと思うので、jQuery Deferrdを使って同じ処理を書いた例を見てください。

function getUstInfo( channel ) {
  var dfr = $.Deferred();
  var api_url = 'http://api.ustream.tv/json/channel/';
  var api = api_url + encodeURIComponent( channel ) + '/getInfo?callback=?';
  $.getJSON( api, dfr.resolve );
  return dfr.promise();
}

function displayUst( embed_tag, target ) {
  $( target ).replaceWith( embed_tag );
}

getUstInfo( 'ぬこのこ' ).then( function( info ) {
  displayUst( info.embedTag, '#ust' );
});

どうですか?
getしてくる部分と表示部分が、しっかり分離されていることがわかると思います。
また、promiseで一旦返しておいて、API呼び出しの答えが返ってきた時点でthenの中身が呼ばれるため、その間に並列で処理を進めることができます。


DeferredはjQuery1.5以上で利用できるようになっています。
また、DojoMochiKitといった他のjavascriptライブラリでも同様の機能が利用できるそうです。