読者です 読者をやめる 読者になる 読者になる

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

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

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

Windows ストアアプリ (Metro スタイルアプリ) における JavaScript の非同期処理 (WinJS.Promise)

Windows 8 Metro スタイルアプリ JavaScript Windows ストアアプリ

概要

Windows 8 の Windows ストアアプリ (旧称 Metro スタイルアプリ) 開発の話です。 Windows ランタイムと JavaScript 用 Windows ライブラリでは非同期処理を行う関数は基本的に WinJS.Promise オブジェクトを返します。 WinJS.Promise は Common JS Promises/A 提案 の実装であり、非同期処理を行う関数がコールバック関数を引数として受け取る場合にコードが難読化するという問題を克服するためのものです。 WinJS.Promise を使用することにより、非同期処理の連鎖を読みやすく書くことができます。

JavaScript で同様のことを行うものとしては JSDeferred とか jQuery.Deferred とかがあって、それらを知っていればすぐに理解できるのではないかと思います。

本記事は WinJS.Promise についてまとめたものです。

参考文献

基本的に Microsoft の公式のドキュメントを見れば事足ります。

WinJS.Promise オブジェクトを返す関数の作り方

WinJS.Promise オブジェクトを返す関数は、基本的に用意されているものを使うことになると思います。

とはいえ、自分で WinJS.Promise オブジェクトを返す関数を書きたくなることもあると思いますが、そのときは WinJS.Promise コンストラクタを使うことで WinJS.Promise オブジェクトを作り、そのオブジェクトを返すような関数を書けばよいです。

WinJS.Promise コンストラクタは 2 つの引数をとります。

1 つめの引数 init は、非同期処理を実際に行い、非同期処理の終了時やエラー時に WinJS.Promise オブジェクトに通知する処理を行う関数オブジェクトです。 この関数は 3 つの関数オブジェクトを受け取ります。 非同期処理に成功した場合には第 1 引数として渡された関数を呼び出して成功を伝え、失敗した場合には第 2 引数として渡された関数を呼び出して失敗を伝えます。 3 つめの引数は、進捗を伝えるための関数で、必ずしも呼び出す必要はありません。 これら 3 つの関数はいずれも 1 つの引数を受け取り、その値は then メソッドで指定されたコールバック関数に渡されます。

2 つめの引数 onCancel は、WinJS.Promise#cancel メソッドなどによりキャンセルされた場合に呼ばれる関数です。 この関数も必ずしも必要なわけではありません。

WinJS.Promise オブジェクトを返す関数の例を示します。 1 個以上の数値を引数にとって、200 ms ごとに 1 個ずつ加算していって、最終的に合計値を求めるという関数です。 非同期処理の例のために考えたのでちょっと無理やり感がありますが、まあ良いでしょう。

後でエラー処理の例を示したいので、合計値が負の値になったときにエラーになるようにしています。 また、合計値を求める処理は setInterval を使って実現しており、際に特に何も処理をしなければキャンセルされても setInterval の処理がそのまま続いてしまうので、キャンセル時の処理も書いています。

/** WinJS.Promise オブジェクトを返す関数: 非同期的に引数の合計を計算する */
function calcSumAsync() {
    var i = 0, args = arguments, len = arguments.length, sum = 0;
    if (len === 0) throw new Error("引数は 1 個以上指定してください");
    var timerId;
    // 非同期処理を行う関数を渡して Promise オブジェクトを作る
    return new WinJS.Promise(function init(comp, err, prog) {
        timerId = setInterval( function () {
            try {
                console.log("非同期に合計値の計算中...");
                sum += args[i];
                if (sum < 0) throw new Error("負の合計はサポートされていません");
                ++i;
                if (i < len) {
                    prog(sum);
                } else {
                    clearInterval( timerId ); timerId = null;
                    comp(sum);
                }
            } catch (e) {
                err(e);
            }
        }, 200 );
    }, function onCancel() {
        log( "キャンセル処理..." );
        clearInterval( timerId ); timerId = null;
    });
}

WinJS.Promise オブジェクトを返す関数の使い方

非同期処理が完了した際に WinJS.Promise オブジェクトから値を取り出すために、then メソッドか done メソッド使用します。

これらのメソッドは、どちらも 3 つの関数オブジェクトを引数にとります。 エラー無く非同期処理が完了した際には第 1 引数の関数 onComplete が呼び出され、エラーが発生した場合には第 2 引数の関数 onError が呼び出されます。 第 3 引数 onProgress は進捗を伝えるために呼び出される関数ですが、必ずしも全ての WinJS.Promise オブジェクトを返す関数が進捗を伝えてくれるわけではありません。 これら引数として指定される 3 つの関数は、いずれも 1 つの引数をとります。 onComplete の場合は非同期処理が返す値を引数として受け取りますし、onError の場合は発生した例外を受け取ります。

WinJS.Promise#then メソッドは新しい WinJS.Promise オブジェクトを返すので、非同期処理を連鎖させることができます。 WinJS.Promise#done メソッドは undefined を返します。 then メソッドと done メソッドでは例外の処理の方法が違うようなのです が、手元で試してみたところいまいちよくわかりませんでした *1

then メソッドに渡した onCompleteonError が WinJS.Promise オブジェクトを返す場合には、その WinJS.Promise オブジェクトが値を取得 (またはエラーになる) してから次の then (または done) に繋がります。 onCompleteonError が WinJS.Promise オブジェクト以外の値を返す場合は、即次の then や done に繋がります。

また、onCompleteonError の値を指定しなかった場合や null を渡した場合などは、そのまま次の then や done に移ります *2

例 : 非同期処理を連鎖させる

// 非同期的に合計を求める
calcSumAsync( 30, 20, 33, 20 ).
then(
    function onSuccess( sum ) { // 合計が求まったらここにくる
        // 得られた合計値にさらに値を追加する (非同期的に)
        return calcSumAsync( sum, 30, 454, 24 );
    }
).
done(
    function onSuccess(sum) { // 前段の onSuccess の中の非同期処理が全て終わったらここへ
        console.log("success" + sum);
    },
    function onError(err) { // 前段の onSuccess の中, もしくは最初の calcSumAsync で例外が発生したらここへ
        console.log("err2:" + err);
    },
    function onProgress(sum) { // 前段の onSuccess の中の非同期処理中の進捗報告はここに
        log("progggg: " + sum);
    }
);

例 : キャンセルする

WinJS.Promise#cancel() メソッドを使って非同期処理を途中でキャンセルすることもできます。 (必ずしもサポートされているわけではない。)

// 非同期的に合計を求める
var promise = calcSumAsync( 30, 20, 33, 20 );
promise.then(
    function onSuccess( sum ) { // 合計が求まったらここにくる
        // 得られた合計値にさらに値を追加する (非同期的に)
        return calcSumAsync( sum, 30, 454, 24 );
    },
    function onError(err) { // 前段の onSuccess の中, もしくは最初の calcSumAsync で例外が発生したらここへ
        console.log("err2:" + err);
    }
);
setTimeout( function () {
    // WinJS.Promise#cancel メソッドを使ってキャンセルさせる
    promise.cancel();
}, 500 );

その他

さらに WinJS.Promise.join メソッドを使って複数の非同期処理が全て終わることを待つとか、WinJS.Promise.timeout メソッドを使って指定時間が経過しても非同期処理が終わらなかった場合にキャンセルするとか、そういうこともできます。 詳細はドキュメントへ。

また、WinJS.Promise では非同期処理をループさせるための関数は提供されていないのですが、やはりループさせたいということは多々ありますのでそのための関数を書きました。 ループさせたいときは以下の記事を参照してください。

*1:then メソッドでも done メソッドでも例外を処理しなかった場合はアプリケーションそのものが終了してしまった

*2:onComplete のみ指定して onError を指定しなかった場合に、次の then で onError が指定されていればそっちに移る