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

第1回JSDeferredによる簡単非同期処理

どうもはじめまして、株式会社はてなのid:cho45です。これから数回に渡り、拙作のJavaScriptのライブラリであるJSDeferredの紹介と、それに関係するJavaScriptにおける非同期の技術に関して解説させていただきます。

JavaSciptにおける非同期処理

非同期処理とは

まずは確認程度に、非同期処理がどういうものかを解説しておきます。先に乱暴にまとめてしまうと、以下の事柄が大切なところです。

  • 非同期とは「あとで」という意味
  • 同期処理はコードの見た目順に実行される/非同期ではそうならない
  • JavaScript の非同期処理は絶対に同期処理にすることができない

JavaScriptにおける非同期処理をいくつか例に出すと、以下のようなものが挙げられます。

リスト1 
// XMLHttpRequest の例
var req = new XMLHttpRequest();
req.open('GET', 'http://example.com/', true);
req.onreadystatechange = function (e) {
  if (req.readyState == 4)
     alert('async!')
  }
};
req.send(null);
alert('hello');
リスト2 
// setTimeout の例
var sid = setTimeout(function () {
  alert('async!');
}, 0);
alert('hello');
リスト3 
// イベントの例
document.body.onclick = function (e) {
  alert('async!');
};
alert('hello');

これらは、すべて非同期で処理されるものです。非同期かどうかは、最後の alert() が、直前にでてくる function () {} の中の alert() よりも先に表示されることで、すなわち上記の例はすべて hello -< async! と表示されることで確かめられます。

コールバック関数を渡す関数は非同期になっていることも多いですが、同期的に実行されるようなものもあるので、コールバックをとるかどうかではなく、実行順で判断します。JavaScriptはシングルスレッドなので、非同期的に呼ばれるコールバックを登録する関数を呼ぶと、そのコールバックは必ず現在の実行されている関数が終わったあとに呼びだされます。

例にあるように setTimeout でタイムアウト時間を 0 にしたとしても、絶対に渡した関数は即時実行(=同期実行)されません。

このコールバックをとって関数を非同期実行させるのは最もシンプルな方法ですが、見ての通りコードにしたときの書き順と、実行順が変化します。

JSDeferredとは。JSDeferredが解決すること

JSDeferredはJavaScript上の、あらゆる非同期処理を扱うライブラリです。Deferredというのは聞き慣れない言葉ですが、これはJSDeferredがMochiKitDeferredを参考に作られているからです。非同期処理において、処理はあとに延期され (deferred) ていくため、と僕は覚えています。

詳しいことはあとあと説明しますが、JSDeferredというライブラリは以下の事柄をコンセプトにしています。

コンパクトであること
単純にコード行数が少ないということです。
スタンドアローンで動くこと
他のどんなライブラリやフレームワークにも依存せずに動くということです。実際、JSDeferred の基本機能は setTimeout の挙動のみに依存しています。
書きやすいこと
コールバックによる非同期処理がめんどうくさくて作ったものなので書きやすくないと意味がありません。

JSDeferred自体のコードを読みとくときや、これからの解説を読むうえでこれらが頭に入っていると、理解が早いかと思います。

JavaScriptにおける非同期処理の問題点

最初、JavaScriptにおける非同期処理の例として、setTimeout などを挙げました。これらは全てコールバックをとり、それをあとで実行していくものです。

例えば以下のようなコードを考えてみます。最終的に foo.json, bar.json, baz.json 全ての情報を得たい、という場合です。http.get は URI とコールバック関数をとり XMLHttpRequest を実行する関数として考えてください。

リスト4 
http.get("/foo.json", function (dataOfFoo) {
  http.get("/bar.json", function (dataOfBar) {
    http.get("/baz.json", function (dataOfBaz) {
      alert([dataOfFoo, dataOfBar, dataOfBaz]);
    });
  });
});

このようにだんだんと関数のネストが深くなっていきます。この程度ならまだいいですが、これがさらに数個あったりしたらどうでしょうか。あるいは、以下のように任意の個数のデータを取得しなくてはならないときはどうでしょうか。

リスト5 
var wants = ["/foo.json", "/bar.json", "/baz.json"];

正直めんどうくさくありませんか?

このように、単純にコールバックを使って非同期の処理をしていく方法だと、複雑な処理を書くのがとても難しくなります。

JSDeferredを使う

JSDeferredは、このようなコールバックの入れ子による非同期処理を、オブジェクトの連鎖による表現に変形させてあげることで、半分同期処理のような感覚で書くことができるようにします。

例えば、⁠foo.json, bar.json, baz.json 全ての情報を得たい」というコードをJSDeferredで書きなおすと、以下のようになります。

リスト6 
// http.get は URI をとって Deferred を返す関数
var results = [];
next(function () {
  return http.get("/foo.json").next(function (data) {
    results.push(data);
  });
}).
next(function () {
  return http.get("/baz.json").next(function (data) {
    results.push(data);
  });
}).
next(function () {
  return http.get("/baz.json").next(function (data) {
    results.push(data);
  });
}).
next(function () {
  alert(results);
});

少々長くなりましたが、全く同じ形の部分が3回でてくるようになっています。同じ形ならば纏めてしまえば良いわけですから、さらに簡潔に以下のように書くことができます。

リスト7 
var wants = ["/foo.json", "/bar.json", "/baz.json"];
var results = [];
loop(wants.length, function (i) {
  return http.get(wants[i]).next(function (data) {
    results.push(data);
  });
}).
next(function () {
  alert(results);
});

この時点でかなり簡潔になりましたし、任意の数のリソースを読みこむ、というのも自然に達成できています。この例のコードは foo.json -> bar.json -> baz.json を順番に読みこむものですが、実際には平行に読みこんで欲しい場合のほうが多いですね。そんなときにはさらに簡潔に、以下のように書けてしまいます。

リスト8 
parallel([
  http.get("/foo.json"),
  http.get("/bar.json"),
  http.get("/baz.json")
]).
next(function (results) {
  alert(results);
});

このように、コード上での非同期の処理の表現を変えつつ、いくつか便利な機能を提供しているのがJSDeferredです。

ちなみに

JSDeferredはjAutoPagerize[1]を作る過程で生まれたいくつかの欲求を解決するために作られました。そのときは、キャッシュを透過的に実装したい、というのが一番の目的でしたが、仕組みを作っているうちに一般化したくなったためにJSDeferredとして作りはじめました。

JSDeferredは何を解決しないか

  • JSDeferredはブラウザ間の挙動の差を吸収するためのライブラリではない
  • JSDeferredは非同期処理の「書き方」を変えるだけ

クロスブラウザのためのライブラリではない

JSDeferredは単機能なライブラリであって、例えばjQueryなどとは違って、イベントリスナをクロスブラウザ対応するためのものや、ほかにもエフェクトなども入っていません。

役割が違うものなので、JSDeferredは、jQueryやTenなど、いわゆるJavaScriptのフレームワークと組み合せて使うことでアプリケーションを作ることになると思います。

「書き方」を変えるだけ

JSDeferredは基本的には「書き方」を変えるためのライブラリです。ただ、⁠書き方」が変わる仕組みをいくつか導入して、より非同期処理を書きやすく、読みやすくしてくれます。

コールバックを受け取る、JSDeferredを使わずに書いたときのような非同期処理の書きかたは、それぞれの非同期処理が依存関係を持っていたりすると、とたんにコールバックがネストされたりして読みにくく、書きにくくなります。

これを解決して、複雑なものをできるだけ複雑にさせないためのライブラリです。

今回はJavaScriptにおける非同期処理の確認と、JSDeferredが何をするかを解説しました。次回からは実際に簡単なJavaScriptのアプリケーションを作りながら、JSDeferredの使いかたを解説をしていきます。

おすすめ記事

記事・ニュース一覧