pig's diary

何でも忘れるので万年初心者ね

npm q べんり

qはPromises/A提案ベースの非同期管理ユーティリティです。
https://github.com/kriskowal/q/wiki/API-Reference
と思ってたら、今はPromises/A+ というものがあるようです。
http://promises-aplus.github.io/promises-spec/

qがあれば、非同期処理が増えてインデントが増えて読みにくくなったり、クロージャ構造で変数を参照し過ぎてリファクタリングしにくいコードになるのを、ある程度防ぐことができます。

以下は、同期処理と非同期処理を順番に処理していく例です。

var Q = require('q');

var step1 = function(v) {
    var d = Q.defer();
    setTimeout(function() {
        d.resolve(v);
    }, 100);
    return d.promise;
};

var step2 = function(v) {
    console.log(v);
};

var step3 = function() {
    var d = Q.defer();
    setTimeout(function() {
        d.resolve('ohh');
    }, 800);
    return d.promise;
};

// 同期的に 'yeah' という文字列を取得するところからスタート
Q.when('yeah')

// 非同期処理。引数に 'yeah' が渡ってくる。ここでは、100ms 後に、もういちど文字列 'yeah' を返す。
.then(step1)

// 同期処理。前のステップが終わってから実行される処理。ここでは、渡ってきた値を出力する。
.then(step2)

// 再び非同期処理。今度は、800ms後に文字列 'ohh' を渡す。
.then(step3)

// お終い。最後のステップで返される文字列 'ohh' が渡ってくる。
.done(function(v) {
    console.log(v, '... done. ');
});

上の例では非同期処理がただのsetTimeoutですが、これが「ファイルアクセス」や「HTTPリクエスト」等の非同期だったとして考えてみてください。

このように、返り値であるpromiseオブジェクトのthenメソッドで連ねていくと、1本道のステップを同期・非同期に関係無く簡単に作ることができます。

更に、qは「非同期処理を並列実行し、2つの処理が終わった直後に、2つの返り値を利用して処理を続行する」という複雑な処理も、簡単に書くことができます。

var Q = require('q');

// ステップ1のA。2000ms 後に、文字列を返す。
var step1A = function() {
    var d = Q.defer();
    setTimeout(function() {
        d.resolve('result of step1A');
    }, 2000);
    return d.promise;
};

// ステップ1のB。700ms 後に、文字列を返す。
var step1B = function() {
    var d = Q.defer();
    setTimeout(function() {
        d.resolve('result of step1B');
    }, 700);
    return d.promise;
};

// ステップ2。並列処理の結果を出力する。
var step2 = function(step1A_result, step1B_result) {
    console.log(step1A_result + ', ' + step1B_result);
}

// ステップ1A と、ステップ1B を並列実行する。
Q.allResolved([step1A(), step1B()])

// 両方の処理が終わったらすぐに、ステップ2 を実行する。
.spread(step2);

これを自前でやろうとすると、おかしなコードになりがちです。2つの並列処理の最後に、お互いの処理が既に終わっているかどうかをチェックし、trueなら第3の処理を続行。しかし2つの処理結果の引数を渡しにくいし、順序も分かりにくいし、コードも重複しがちです。さらに、並列処理が10個だとどうでしょうか? きっと何もかもが嫌になり家に帰って10時間眠りたくなるでしょう。qがあれば、安全かつ最速でステップを進めることができます。

安全に、と言えば、qはステップの途中に処理を離脱するコードも1カ所に書いておけるのも魅力です。

var Q = require('q');

// ランダムで、OKあるいはNGになるステップを作成。
var okOrNgStep = function() {
    var d = Q.defer();
    var timeout = Math.random() * 1000;
    setTimeout(function() {

        // 1:1 の確率で、OK あるいは NG と判断します。
        timeout > 500 ? d.resolve('More than 500!') : d.reject('timeout <= 500..');
    }, timeout);
    return d.promise;
};

Q.when()

// OKあるいはNGになるステップを実行。
.then(okOrNgStep)

// OKなら、こちらにステップが進む。
.then(function(msg) {
    console.log('OK! ', msg); 
})

// NGなら、こちらにステップが進む。
.fail(function(msg) {
    console.log('NG... ', msg);
})

// 最後には必ずここに来る。
.done(function() {
    console.log('... done.');
});

これは、ステップのどこかで起きた例外のハンドリングのためにとても有用です。仮に10ステップの長いフローがあったと考えてみてください。もしこの機能が無ければ、全てのステップの中に if (err) handleError(err); と書いておかねばならないでしょう。

q以外にも、非同期ユーティリティは他にもたくさんあるようです(https://github.com/joyent/node/wiki/modules#wiki-async-flow)。しかし私が触ったいくつかのnpmモジュールは qを使っていたので、比較的広く導入されているんじゃないかと思います。

個人的には、引数の取り方が柔軟過ぎる気もしますが、それほど問題にならないのかも知れません。

あなたは非同期管理をどうしていますか?