JavaScriptの非同期処理
JavaScriptにおける非同期処理のライブラリは非常にたくさん乱立していて、一体どれを使えばいいのか判断がつきません。
(本記事はその答えを明確に示すものではないので、もしそういうのを期待してたらごめんなさい)
ライブラリはたくさんありますが、いくつかの方式にカテゴライズすることができそうです。(ここに挙げてない方式もきっとあるでしょう)
-
いつか値を返すのでとりあえずプレースホルダを受け取っておいてよ方式 :promise, jQuery.Deferred, goog.result など
-
そんなことよりFunctional Reactive Programmingだ!方式 :RxJS, Bacon.js など
はて、こんな対立構造をどこかで見たことあるなあと思ったら、C#でも以前同じような議論がありました。
それぞれ対応付けてみるとこんな感じですね。
C# | JavaScript |
---|---|
Task | promiseなど |
async/await | coなど |
Reactive Extensions | RxJSなど |
各方式は対立するのか?
ところでこれらの方式って、必ずしも対立するってわけじゃないんですよね。
JavaScriptのgeneratorはまだ使えるところが限られるけど、コールバックが不要で同期処理のように簡単に書けるのはやはり魅力的です。
一方で、ちょっとした非同期処理をReactive Programmingで処理するのはおおげさですが、ストリーム的に流れてくる複数のイベントを合成したり、時間的な操作をしたりする場合には圧倒的に便利です。
なので、適材適所で使い分けるのがよさそうです。(なんの解決にもなってない!)
promise, co, RxJS
さて、使い分けると言っても、方式が異なるものを混ぜて使うことは可能でしょうか?
実は、それぞれの方式は対立しないどころか、連携することも可能です。
各方式の代表的な実装であるpromise, co, RxJSを例にみてみましょう。
まず、promiseとRxJSですが、RxJSにObservable.fromPromise(),Observable.toPromise()というAPIが用意されているので、それを呼ぶだけで相互に変換が可能です。
次に、coはyieldableなオブジェクトを扱うことができるそうです。(参考:いぇーい yield と co と koa)
なのでpromiseはそのままcoで扱うことができるし、次のような変換関数を用意しておけば、RxJSのObservableをcoで扱うことも可能になります。
function observableToThunk(observable) {
return function(fn){
var d = observable.subscribe(function(res) {
d.dispose();
fn(null, res);
},
function(err) {
d.dispose();
fn(err);
});
}
}
これをcoの中で呼ぶようにしてあげます。
そうすると、RxJSのObservableが同期処理のように書けます。エラーもtry/catchで処理できます。
ただしストリーミング的な非同期処理はできなくなるので注意です。
co(function *() {
var ret = yield Rx.Observable.return(new Date()).delay(3000);
console.log("date: " + ret);
})();
※coをchromeで動作させるためには、chrome://flagsの#enable-javascript-harmonyを有効にする必要があります。
連携例
promise, co, RxJSの全部を組み合わせると、こんなことができたりします。
- http通信の結果をpromiseで取得する(AngularJSの$httpサービスを想定)
- 1つめのhttp通信の結果を使って2つめのhttp通信を行う
- RxJSを使ってイベント合成など複雑な処理を行う
- coを使うことで最終結果の取得とエラー処理を同期処理のように書く
co(function *() {
try {
var id = yield $http.get("/getid");
var promise = $http.get("/items/" + id);
var obs = Rx.Observable.fromPromise(promise)
.zip(mouseEvent, function (received, ev) { return received; })
.throttle(1000)
.select(JSON.parse);
var ret = yield obs;
console.log(ret);
} catch (e) {
console.log("error: " + e);
}
})();
うまく連携できてますね!