ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

WinJS.Promise による非同期処理をループさせる関数 (Windows ストアアプリ)

JavaScript で非同期処理を chainable に書くためのライブラリとして、JSDeferredjQuery.Deferred があります。 Windows ストアアプリ (旧称 Metro スタイルアプリ) における JavaScript 用ライブラリである WinJS では、同様のものが WinJS.Promise として提供されています。

WinJS.Promise に関して詳しいことは以前に書きましたのでそちらを参照してください。

WinJS.Promise には非同期処理をループさせる関数がないので書いた

WinJS.Promise では色々な機能が提供されていますが、非同期処理を繰り返すための関数は提供されていません。

JSDeferred.loop メソッド のように非同期処理をループさせる関数を WinRT 環境でも使いたかったので、WinJS.Promise による日同期処理をループさせる関数を書きました。

function loopAsync(initVal, fun) {
    if (typeof initVal === "function" && typeof fun === "undefined") {
        fun = initVal;
        initVal = void 0;
    }
    var toBeStopped = false;
    var canceled = false;
    var loopController = {
        get stopLoop() {
            return function () { toBeStopped = true };
        },
        count: 0,
        prevStepValue: void 0
    };
    Object.seal(loopController);
    // cancel できるように Promise を保持しておくための変数
    var processingPromise = null;
    // ここより上では例外を出さないように
    return new WinJS.Promise(
        function (success, error, prog) {
            // この中から例外を送出した場合は Promise のチェインの後ろに ErrorPromise が伝搬する

            var c = 0;
            f(initVal);

            function f(value) {
                loopController.prevStepValue = value;
                loopController.count = c;
                if (canceled) {
                    return;
                }
                // timeout メソッドを使うことで関数呼び出しごとにスタックが深くなることを防ぐ
                processingPromise = WinJS.Promise.timeout(0).then(function () {
                    return fun(loopController);
                });
                processingPromise.done(function (val) {
                    processingPromise = null;
                    if (toBeStopped) {
                        success(val);
                        return;
                    }
                    c++;
                    f(val);
                }, function onError(err) {
                    processingPromise = null;
                    // canceled の場合は外側で ErrorPromise にしてくれるのであえて error を呼び出さなくてもよい
                    if (!canceled) {
                        error(err);
                    }
                });
            }
        },
        function onCancel() {
            canceled = true;
            if (processingPromise) processingPromise.cancel();
        }
    );
}

使い方

loopAsync は引数を 1 つか 2 つとります。 引数が 1 つの場合、引数はループの 1 ステップに相当する関数とします。

この関数には 1 つのオブジェクトが引数として渡されます。 引数として渡されるオブジェクトは以下のメソッドとプロパティを持ちます。

  • stopLoop メソッド : このメソッドを呼び出すと、そのステップでループが終了する
  • count プロパティ : 何回目のステップかを表す値で、初回のステップは 0
  • prevStepValue プロパティ : 現在のステップの前のステップで関数から返された値

初回のステップにおける prevStepValue の値は、loopAsync メソッドに引数を 1 つだけ渡した場合は undefined となります。

loopAsync メソッドを 2 引数で呼び出した場合、第 1 引数が初回のステップにおける prevStepValue の値になります。 第 2 引数は、ステップごとの処理を行う関数です。

ステップごとの処理を行う関数の返り値は、通常の Promise のコールバックと同じように、通常の値を返しても WinJS.Promise オブジェクトを返しても良いです。 通常の値が返された場合はその値がそのまま次のステップに渡されますし、WinJS.Promise オブジェクトが返された場合は Promise に包まれている値が次のステップに渡されます。

サンプルコード

ステップごとに待ち時間を入れながら 11 回ループする例。

loopAsync( 0, function ( controller ) {
    // controller.prevStepValue は前のステップの返り値
    // controller.count はステップの繰り返し回数 (初回が 0)
    var sum = controller.prevStepValue + controller.count;
    if ( controller.count >= 10 ) {
        // このステップでループを終了するように
        // (break 文とは違って, このメソッドを呼び出した瞬間終わるわけではないので注意)
        controller.stopLoop();
    }
    // 1000 ms 待ってから次のステップに; 次のステップには prevStepValue として sum の値が渡される
    return WinJS.Promise.timeout( 1000 ).then(function () {
        return sum;
    });
} ).
done(function ( sum ) {
    // 合計は sum
});

Promise のキャンセル

Promise#cancel メソッドが呼び出された場合にちゃんとループの処理が止まるようにしています。

var loopPromise = loopAsync( 0, function ( controller ) {
    var sum = controller.prevStepValue + controller.count;
    if ( controller.count >= 10 ) {
        controller.stopLoop(); // このメソッドを呼び出して、即このステップが終了するわけではないので注意
    }
    return WinJS.Promise.timeout( 1000 ).then(function () {
        return sum;
    });
} );
loopPromise.done(function ( sum ) {
    // 合計は sum
}, function onError ( err ) {
    // エラー
    // Promise がキャンセルされた場合もここにくる
});

// キャンセルしたい場合は cancel メソッドを呼び出す
loopPromise.cancel();