promise の概要 (JavaScript による Windows ストア アプリ向け)

Windows ストア アプリを JavaScript で記述する場合、非同期 API を含む処理を記述しようとすると、その種類にかかわらずすぐに promise (英語) と呼ばれるコンストラクターに遭遇します。間もなく、連続する非同期操作で自然にプロミスのチェーンを使うようになります。

また開発作業の中では、実際にどのような処理が行われているか完全には理解できないような promise の使用方法に出会うことも珍しくありません。そうした例が、HTML ListView のパフォーマンス最適化のサンプル (英語) にある、ListView コントロールのアイテム レンダリング関数の最適化です。これに関する記事も今後公開する予定ですが、//build 2012 で Josh Williams 氏が説明した WinJS の Deep Dive (英語) も参考になります (多少変更されています)。

 list.reduce(function callback (prev, item, i) {
    var result = doOperationAsync(item);
    return WinJS.Promise.join({ prev: prev, result: result}).then(function (v) {
        console.log(i + ", item: " + item+ ", " + v.result);
    });
})

このスニペットでは、プロミスを結合して非同期並行操作を実行し、その結果を list の順序に従って連続的に提供しています。このコードを見て何をしているかがすぐに理解できる方は、この記事全体を飛ばしていただいて結構です。そうでない方は、promise の実際の動作と JavaScript 用 Windows ライブラリ (WinJS) での表記方法を詳しく見ていき、これらのパターンを理解できるようになりましょう。

promise とは、"約束" によって関係が構築されること

まずは、promise はコード コンストラクターまたは呼び出し規約に過ぎないという根本的な真実を押さえましょう。つまり promise は非同期操作に対する継承関係を持たないため、この点ではとても便利です。promise は、将来のある時点で利用できる値、または既に利用できる値を表したオブジェクトに過ぎません。そのため、promise (約束) の使われ方は、私たちがこの言葉を日常生活で使う方法とまったく同じになります。"12 個のドーナッツを渡す約束" と言えば、私が今すぐドーナッツを 12 個持っている必要はありません。これは、将来のいつかの時点でドーナッツを入手するという意志があるという意味になります。入手したら、それらを届けます。

2 つのエージェント間に関係があることを示しています。2 つのエージェントとは、品物を届けることを約束したオリジネーターと、約束の対象者であり品物の受領者であるコンシューマーです。オリジネーターが品物を入手する方法は、オリジネーターに任されます。同様に、コンシューマーは、promise 自体と届いた品物を好きなように活用できます。たとえば、他のコンシューマーと promise を共有することもできます。

オリジネーターとコンシューマーの間の関係には "作成 (creation)" と "履行 (fulfillment)" という 2 つの段階があります。このすべてのしくみは以下の図で説明できます。

promise_diagram

図のフローを追っていくと、2 つの段階を持つ promise が非同期操作の提供と適切に連携できる理由を理解できます。重要な部分は、要求の確約 (promise) を受け取ったコンシューマーは、待機 (同期) せずにそれまでの処理を続行 (非同期) できるという点です。これは、コンシューマーが promise の履行を待ちながら、別の要求への応答など別の処理を実行できることを意味します。これは非同期 API の根本的な目的そのものです。既に品物を入手できる場合は promise がすぐに履行されるので、全体が同期呼び出し規約の一種に相当することになります。

もちろん、この関係の場合、考慮が必要なことは多少増えます。日常生活では、いくつかの約束を引き受け、自分に対してもいくつかの約束をしてもらいます。これらの約束の多くは果たされますが、多くの約束が果たされないこともまた現実です。注文したピザは、なんらかのアクシデントがあれば届けられません。日常生活でも非同期プログラミングにおいても、破られた約束は破られたものとして受け入れなければなりません。

これについて promise の関係で考慮すべきことは、1 つは、"ごめんなさい。この promise を履行できませんでした" というメッセージをオリジネーターがなんらかの方法で伝える必要があり、また、約束が破られたことをコンシューマーが何らかの方法で知る必要があること、2 つ目は、コンシューマーが promise の履行をいつでも気長に待てるとは限らないこと (このため、promise の履行の進行をオリジネーターが追跡できる場合、その情報を受け取る方法がコンシューマーにも必要です)、3 つ目は、コンシューマーは注文をキャンセルして、品物が不要になったことをオリジネーターに伝えてもかまわないことです。

これらの要件を追加すると、完全な関係図ができあがります。

promise_diagram_2

では、この関係がコードではどのように表現されるかを見ていきましょう。

promise コンストラクターと promise のチェーン

promise については非常に多くの異なる提案がなされ、仕様が発表されています。Windows と WinJS が採用している Common JS/Promises A (英語) と呼ばれる仕様では、promise とは、将来提供する値を表現するためにオリジネーターが返すものであり、then と呼ばれる関数を持つオブジェクトであるとしています。コンシューマーは、then を呼び出すことによって promise の履行にサブスクライブします (Windows の promise は、同様の関数として done もサポートしています。promise のチェーンで使用されるこの関数については、後ほど簡単に説明します)。

この関数に対して、コンシューマーは最大 3 つのオプションの関数を以下の順序で渡すことができます。

  1. completed ハンドラー: オリジネーターは promise された値が利用可能になった場合にこの関数を呼び出します。また、この値が既に利用可能な場合、then からすぐに (同期的に) completed ハンドラーが呼び出されます。
  2. error ハンドラー (オプション): promise された値を取得できなかった場合に呼び出されます。あらゆる promise では、error ハンドラーが呼び出された場合 completed ハンドラーはいかなる場合も呼び出されません。
  3. progress ハンドラー (オプション): 中間結果と共に定期的に呼び出されます (操作がサポートしている場合。WinRT では、この API に IAsync[Action | Operation]WithProgress の戻り値がある場合はサポートしており、IAsync[Action | Operation] の場合はサポートしていません)。

error ハンドラーだけをアタッチし completed ハンドラーをアタッチしない場合は、これらの引数に null を渡すことができます。

この関係では、コンシューマーは then を複数呼び出すことによって、同じ promise に任意の数の promise をサグスクライブすることができます。さらに promise を、重要コンテンツに対して then を呼び出す別のコンシューマーと共有することもできます。この処理は完全にサポートされています。

これは、promise が、受け取るすべてのハンドラーのリストを管理し、適切なタイミングで呼び出す必要があるということです。さらに promise は、完全な関係の図で説明したキャンセルにも対応する必要があります。

その他 Promises A 仕様には、then メソッド自身が promise を返すという要件も規定しています。この 2 つ目の promise は、最初の promise.then に付与された completed ハンドラーが返され、この 2 つ目の promise の結果として戻り値が提供された時点で履行されます。以下のコード スニペットをご確認ください。

 var promise1 = someOperationAsync();
var promise2 = promise1.then(function completedHandler1 (result1) { return 7103; } );
promise2.then(function completedHandler2 (result2) { });

ここでは、someOperationAsync が開始され promise1 が返されることで実行が連結されます。操作の実行中に promise1.then を呼び出すと、その直後に promise2 が返されます。非同期操作の結果が既に利用可能な場合を除き、completedHandler1 は呼び出されないことに注意してください。ここでは引き続き待機している状態と仮定します。すぐに promise2.then の呼び出しに移りますが、この時点でも completedHandler2 は呼び出されません。

しばらくして someOperationAsync がたとえば値 14618 にて完了します。これで promise1 が履行されるため、この値で completedHandler1 を呼び出し、result1 が 14618 になります。今度は completedHandler1 が実行され、値 7103 を返します。この時点で promise2 が履行され、completedHandler2 が (7103 に等しい) result2 で呼び出されます。

では、completed ハンドラーが別の promise を返した場合について説明します。この場合は処理が多少変わります。上記のコードの completedHandler1 が以下のような promise を返した場合を考えてみます。

 var promise2 = promise1.then(function completedHandler1 (result1) {
    var promise2a = anotherOperationAsync();
    return promise2a;
});

この場合、completedHandler2 の result2 は promise2a そのものではなく、promise2a が履行された値です。つまり、completed ハンドラーが promise を返すため、promise1.then から返される promise2 は、promise2a の結果によって履行されます。

連続する非同期操作の連結を可能にしているのは、チェーンに含まれる各操作の結果が次の操作に適用されるという、まさにこの特性です。中間変数や名前付きハンドラーがない場合に多く見られるのは、以下のようなパターンの promise チェーンです。

 operation1().then(function (result1) {
    return operation2(result1)
}).then(function (result2) {
    return operation3(result2);
}).then(function (result3) {
    return operation4(result3);
}).then(function (result4) {
    return operation5(result4)
}).then(function (result5) {
    //And so on
});

各 completed ハンドラーは、それぞれが受け取る結果を使用してさらに何かを実行する場合がほとんどですが、あらゆるチェーンの基本的な構造はこのようになります。また、ここにあるすべての then メソッドは、順番に実行され、特定の completed ハンドラーの保存と別の promise を返すという処理を行っています。このため、このコードの末尾に到達するまでに開始されているのは operation1 だけで、completed ハンドラーは 1 つも呼び出されていません。しかし、すべての then 呼び出しからの中間 promise が多数作成され、相互に関連付けられることによって、チェーンが管理され連続的な操作が進行しています。

後続の操作を直前の completed ハンドラー内に入れ子にすることで、同様のシーケンスを実現できる点に注目してください。この場合はすべての return ステートメントを使いません。ただし、このような入れ子処理は、特に then への呼び出しの 1 つひとつに progress ハンドラーと error ハンドラーを追加するような場合、インデントが非常に複雑になります。

また、WinJS の promise の機能では、チェーンのあらゆる部分で発生したエラーは、チェーンの末尾まで自動で伝播します。これは、then の最後の呼び出しに error ハンドラーを 1 つアタッチするだけで、すべてのレベルにハンドラーを設定する必要がなくなるということです。ただし、チェーンの最後のリンクが then への呼び出しである場合、このような要求は、さまざまな理由によって消去されてしまいます。このため WinJS では promise に done メソッドを用意しています。このメソッドは then と同じ引数を受け入れますが、示すのはチェーンの完了です (別の promise ではなく undefined を返します)。その後、チェーン全体のあらゆるエラーに対して、done にアタッチされたすべての error ハンドラーが呼び出されます。さらに、error ハンドラーが足りないことで、done はアプリケーション レベルで例外をスローし、この例外は WinJS.Application.onerror イベントの window.onerror によって処理されます。つまり、例外が特定され適切に処理されるためには、すべてのチェーンが done で終わることが理想的です。

もちろん、長い then 呼び出しチェーンから最後の promise を返すことを目的とする関数を記述する場合、末尾に then を使用します。この場合のエラー処理は、その promise を別のチェーンで使用する呼び出し元が引き受けます。

promise の作成: WinJS.Promise クラス

Promises A 仕様に基づき独自の promise クラスはいつでも作成できますが、非常に煩雑な作業になるため、ライブラリがあれば便利です。WinJS には堅牢かつ入念にテストされた、WinJS.Promise (英語) と呼ばれる柔軟な promise クラスがあります。これを活用することで、オリジネーター/コンシューマーの関係や then の動作を詳細に管理せずに、さまざまな値と操作の promise を簡単に作成できるようになります。

非同期操作と既存の (同期的な) 値の両方について promise を作成する際には、必要に応じて new WinJS.Promise またはヘルパー関数 (次のセクションで説明) を使用できます (使用する必要があります)。改めて、promise はコード コンストラクターに過ぎません。promise が非同期操作を含むあらゆる非同期的な処理をラップしなければならないという要件はありません。同様に、まれなケースとして promise のコードの一部をラップしても、それらが自動的に非同期で実行されることはなく、個別に対応する必要があります。

WinJS.Promise を直接使用する簡単な例として、処理の長い計算を考えてみます。ここでは 1 から順に特定の最大値まで数字を追加していく計算を、非同期で実行します。このような定形処理には独自のコールバック メカニズムを作成することもできますが、promise にラップすることで、この promise をその他の API からの別の promise と連結または結合できます (ここでは、WinJS.xhr (英語) 関数が JavaScript の非同期の XmlHttpRequest をラップするため、別の promise の特定のイベント構造は無視できます)。

処理の長い計算にはもちろん JavaScript ワーカーを使用できますが、説明上の都合から、JavaScript ワーカーは UI スレッド上に保持し、setImmediate を使用して操作を複数のステップに分割します。WinJS.Promise を使用して promise の構造内に実装する方法を以下に示します。

 function calculateIntegerSum(max, step) {
    //The WinJS.Promise constructor's argument is an initializer function that receives 
    //dispatchers for completed, error, and progress cases.
    return new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) {
        var sum = 0;

        function iterate(args) {
            for (var i = args.start; i < args.end; i++) {
                sum += i;
            };

            if (i >= max) {
                //Complete--dispatch results to completed handlers
                completeDispatch(sum);
            } else {
                //Dispatch intermediate results to progress handlers
                progressDispatch(sum);
                setImmediate(iterate, { start: args.end, end: Math.min(args.end + step, max) });
            }
        }
            
        setImmediate(iterate, { start: 0, end: Math.min(step, max) });
    });
}

new WinJS.Promise を呼び出す際のコンストラクターは initializer 関数 (この場合は非同期) です。実行する操作は initializer によってカプセル化されますが、この関数自体は UI スレッド上で同期的に実行されます。このため、ここで setImmediate を使用せずに長い時間の計算を実行する場合は、UI スレッドを常にブロックします。繰り返しになりますが、promise 内にコードを配置しても自動的に非同期で実行されるようにはなりません。initializer 関数で個別に設定する必要があります。

引数については、initializer 関数は、promise がサポートする、完了 (completed)、エラー (error)、進行中 (progress) に対応した 3 種類のディスパッチャーを受け取ります。コードを見るとわかるように、これらのディスパッチャーが、操作の適切なタイミングで適切な引数によって呼び出されています。

これらの関数を "ディスパッチャー" と呼ぶ理由は、コンシューマーが promise の then メソッド (done もですが、以降は省略します) にサブスクライブするハンドラーとは異なるためです。内部的には、これらのハンドラーを WinJS が複数管理することで、任意の数のコンシューマーによる任意の数のハンドラーのサブスクライブが可能になっています。これらのディスパッチャーのいずれかを呼び出すと、WinJS は内部リストを反復処理し、これらのハンドラーすべてをユーザーに代わって呼び出します。また、連結を実現するために必要な別の promise を then が確実に返すようになるのも、WinJS.Promise があるためです。

まとめると、WinJS.Promise は、promise に伴う細かな処理をすべて提供しています。これによってユーザーは、initializer 関数に埋め込まれた、promise が示す重要な操作だけに集中できるようになります。

promise 作成用のヘルパー

promise を作成する主なヘルパー関数は、promise 内のすべての値をラップする静的メソッド WinJS.Promise.as (英語) です。既存の値に適用されたラッパーは、挙動を変え、then に渡されたすべての completed ハンドラーを呼び出します。これによって、既知の任意の値を promise として扱い、これらをその他の promise と (結合または連結を通して) 混合および組み合わせられるようになります。既存の promise に as を使用すると、その promise だけが返されます。

もう 1 つの静的ヘルパー関数は、setTimeoutsetImmediate に関する便利なラッパーを提供する WinJS.Promise.timeout (英語) です。特定のミリ秒以内に履行されない場合に 2 つ目の promise をキャンセルする promise を作成することも可能です。

setTimeout および setImmediate に関する timeout promise は、undefined によって履行される点に注意してください。このとき、これらを使用して、タイムアウト後に別の結果を提供するためにはどうすればよいかという疑問が浮かびます。この疑問に対する回答には、then が、completed ハンドラーの戻り値で履行される別の promise を返すという事実が使用されます。このコード行の例を以下に示します。

 var p = WinJS.Promise.timeout(1000).then(function () { return 12345; });

ここでは promise "p" が 1 秒後に値 12345 によって履行されます。言い換えると、WinJS.Promise.timeout(…).then(function () { return <value>} ) のパターンによって、規定されたタイムアウトの後に <value> が提供されます。<value> 自身が別の promise の場合は、タイムアウト後の特定時点における、その promise の履行値が提供されることになります。

キャンセルと promise エラーの生成

これまで見てきたコードでは、足りない点が 2 つあることにお気付きかもしれません。1 つ目は開始された操作をキャンセルする方法がないこと、2 つ目はエラーの処理が洗練されているとは言えないことです。

これら両方の原因は、promise を生成する calculateIntegerSum などの関数が常に promise を返す必要があるということです。操作が完了できない、または操作が最初に開始されない場合、その promise はエラー状態にあります。これは、呼ばれるのは error ハンドラーだけで、任意の completed ハンドラーに渡すことができる結果を promise が得られず、これからも得られないということを意味します。実際、コンシューマーが既にエラー状態にある promise で then を呼び出すと、promise はすぐに (同期的に) then に付与された error ハンドラーを呼び出します。

WinJS.Promise は、コンシューマーが cancel メソッドを呼び出すか、initializer 関数内のコードが error ディスパッチャーを呼び出すという 2 つの理由でエラー状態になります。エラーが発生すると、promise でどのようなエラー値が取得または伝播されたかに関係なく、error ハンドラーが付与されます。WinJS.Promise 内で操作を作成している場合、WinJS.ErrorFromName (英語) のインスタンスを使用することも可能です。これは、エラーを特定する name プロパティと詳細情報を格納する message プロパティを含む単なる JavaScript オブジェクトです。たとえば promise がキャンセルされると、error ハンドラーは、name と message の名前の両方が "Canceled" に設定されたエラー オブジェクトを受け取ります。

では、操作を最初に開始できない場合はどうすればよいのでしょうか。たとえば calculateIntegerSum を無効な引数 (0, 0 など) で呼び出した場合、カウントの開始さえ試行されずに、エラー状態の promise が返されます。静的メソッド WinJS.Promise.wrapError (英語) はこのような場合に使用します。ここでは、WinJS.ErrorFromName のインスタンスを使用して、この場合は新しい WinJS.Promise インスタンスの代わりに、エラー状態の promise を返します。

これについてもう 1 つ重要なのが、promise の cancel メソッドへの呼び出しによって promise 自身がエラー状態になる一方で、進行している非同期操作はどのように停止できるのかという疑問です。前述の calculateIntegerSum の実装では、作成した promise の状態に関係なく、操作が完了するまで setImmediate の呼び出しが継続されます。実際、promise のキャンセル後に操作によって complete ディスパッチャーが呼び出された場合、promise はその完了を無視します。

ここで求められるのは、処理を続行する必要がなくなったことを promise から操作に伝える方法です。このために WinJS.Promise コンストラクターは、promise がキャンセルされると呼び出される、関数の 2 つ目の引数を使用します。今回の例では、この関数への呼び出しによって setImmediate に対する次の呼び出しを防止し、計算を停止する必要があります。この処理を、適切なエラー処理と共に以下に示します。

 function calculateIntegerSum(max, step) {
    //Return a promise in the error state for bad arguments
    if (max < 1 || step < 1) {
        var err = new WinJS.ErrorFromName("calculateIntegerSum", "max and step must be 1 or greater");
        return WinJS.Promise.wrapError(err);
    }

    var _cancel = false;

    //The WinJS.Promise constructor's argument is an initializer function that receives 
    //dispatchers for completed, error, and progress cases.
    return new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) {
        var sum = 0;

        function iterate(args) {
            for (var i = args.start; i < args.end; i++) {
                sum += i;
            };

            //If for some reason there was an error, create the error with WinJS.ErrorFromName
            //and pass to errorDispatch
            if (false /* replace with any necessary error check -- we don’t have any here */) {
                errorDispatch(new WinJS.ErrorFromName("calculateIntegerSum (scenario 7)", "error occurred"));
            }

            if (i >= max) {
                //Complete--dispatch results to completed handlers
                completeDispatch(sum); 
            } else {
                //Dispatch intermediate results to progress handlers
                progressDispatch(sum);

                //Interrupt the operation if canceled
                if (!_cancel) {
                    setImmediate(iterate, { start: args.end, end: Math.min(args.end + step, max) });
                }
            }
        }
            
        setImmediate(iterate, { start: 0, end: Math.min(step, max) });
    },
    //Cancellation function for the WinJS.Promise constructor
    function () {
        _cancel = true;
    });
}

総括すると、WinJS.Promise のインスタンスを作成することで、さまざまな使用方法が提供されます。たとえば、その他の非同期的手法で Web サービスを利用するライブラリがある場合、これらの操作を promise にラップすることができます。また、新しい promise を使用して、ソースの異なる複数の非同期操作 (または別の promise) を 1 つの promise に結合し、すべての関係を管理することができます。WinJS.Promise の initializer のコード内では、その他の非同期操作とその promise のハンドラーを独自に設定できます。これらを使用することで、ネットワーク タイムアウトなどを自動で再試行するメカニズムのカプセル化や、進行状況を示す汎用の UI の呼び出し、内部的なログ記録や分析の追加が可能になります。これらのすべてでは、処理の詳細をコードの残りの部分が認識する必要はなく、コンシューマーの立場で promise に対応するだけで問題ありません。

また、JavaScript ワーカーを promise にラップして、WinRT のその他の非同期操作と同様の外観と動作を与えることも簡単です。ご存知のように、ワーカーでは、postMessage 呼び出しによってアプリのワーカー オブジェクトに message イベントを発生させることで結果を提供します。以下のコードでは、このメッセージで提供される結果にかかわらず履行される promise に、このイベントをリンクさせています。

 // This is the function variable we're wiring up.
var workerCompleteDispatch = null;

var promiseJS = new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) {
    workerCompleteDispatch = completeDispatch;
});

// Worker is created here and stored in the 'worker' variable

// Listen for worker events
worker.onmessage = function (e) {
    if (workerCompleteDispatch != null) {
        workerCompleteDispatch(e.data.results); /* event args depends on the worker */
    }
}

promiseJS.done(function (result) {
    // Output for JS worker
});

このコードを拡張してワーカーのエラー処理を含めることで、その他の変数における error ディスパッチャーを省略し、message イベント ハンドラーによるイベント引数のエラー情報のチェックを実行し、complete ディスパッチャーの代わりに必要に応じて error ディスパッチャーを呼び出せるようになります。

並列した promise の結合

promise は多くの場合非同期操作のラップに使用されるため、複数の操作を並行して実行することが可能です。この場合に知る必要があるのは、グループ内のいずれか 1 つの promise が履行されるタイミング、またはグループ内のすべての promise が履行されるタイミングです。これを可能にするのが、静的関数 WinJS.Promise.any (英語) および WinJS.Promise.join (英語) です。

これらの関数は値の配列または値のプロパティが設定されたオブジェクトを受け入れます。これらの値は promise にすることができ、promise 以外の値は WinJS.Promise.any によってラップされ、配列またはオブジェクト全体が promise による構成になります。

any の特性を以下に示します。

  • any によって 1 つの promise が作成されます。この promise は、その他の promise の 1 つが履行された場合またはエラーによって失敗した場合に履行されます (論理 OR)。基本的には、any は、これらすべての promise に completed ハンドラーをアタッチし、1 つの completed ハンドラーが呼び出された直後に、any の promise 自身が受け取ったすべての completed ハンドラーを呼び出します。
  • any の promise が履行された後 (リスト内の最初の promise が履行された後)、リスト内のその他の操作は実行を継続し、これらの promise に個別に割り当てられている completed、error、または progress の各ハンドラーを呼び出します。
  • any からの promise をキャンセルした場合は、リスト内のすべての promise がキャンセルされます。

join の特性を以下に示します。

  • join によって 1 つの promise が作成されます。この promise は、その他のすべての promise が履行された場合またはエラーによって失敗した場合に履行されます (論理 AND)。基本的には、join は、これらすべての promise に completed ハンドラーと error ハンドラーをアタッチし、これらすべてのハンドラーが呼び出されるのを待機し、その後、自身が受け取るすべての completed ハンドラーを呼び出します。
  • join の promise は提供されたすべての progress ハンドラーに進行状況をレポートします。この場合の中間結果は、これまで履行された個々の promise の結果の配列となります。
  • join からの promise をキャンセルした場合は、保留中のその他すべての promise がキャンセルされます。

anyjoin に加えて、簡単に使用できる以下 2 つの静的 WinJS.Promise メソッドを覚えておくと便利です。

  • is (英語): 任意の値が promise かどうかを、ブール値を返すことで判定します。基本的には、"then" の名前を持つ関数が設定されたオブジェクトかどうかを確認し、"done" の場合のテストは実行しません。
  • theneach (英語): completed、error、および progress の各ハンドラーを (then を使用して) promise のグループに適用し、promise 内の別の値のグループとして結果を返します。いずれのハンドラーでも null が許可されます。

並列した promise の連続的な結果

WinJS.Promise.join および WinJS.Promise.any によって、並列した promise による作業、つまり非同期並行操作が可能になります。join によって返される promise が履行されるのは、配列内のすべての promise が履行されたときです。ただし、これらの promise は多くの場合ランダムな順序で履行されます。ここで、このように実行できる一連の操作があるものの、その結果を処理する順序は詳細に定義したい (配列内に promise が表示される順序にしたい) 場合はどうすればよいでしょうか。

これを実行するには、後続の promise を、直前のすべての promise の join に結合する必要があります。これは、この記事の最初に示したコードの一部を使用することで実現します。そのコードを改めて以下に示します。このコードは、promise を明示的にするための変更が行われています (list は、promise を生成する架空の非同期呼び出し doOperationAsync の引数として使用される値の配列とします)。

 list.reduce(function callback (prev, item, i) {
    var opPromise = doOperationAsync(item);
    var join = WinJS.Promise.join({ prev: prev, result: opPromise});

    return join.then(function completed (v) {
        console.log(i + ", item: " + item+ ", " + v.result);
    });
})

このコードを理解するためには、配列の reduce (英語) メソッドがどのように動作するかを理解する必要があります。配列の各項目について、reducecallback という関数の引数を呼び出し、4 つの引数を受け取ります (このコードでは 3 つのみ使用されています)。

  • prev: callback への以前の呼び出しから返された値です (最初の項目では null です)。
  • item: 配列からの最新の値です。
  • i: リスト内の項目のインデックスです。
  • source: 元の配列です。

リスト内の最初の項目では、opPromise1 と呼ばれる promise が取得されます。prev は null のため、 [WinJS.Promise.as(null), opPromise1] を連結します。ただし、join 自体が返されているわけではなく、completed ハンドラー (この場合 completed) を join にアタッチし、その then から promise を返しています。

then から返される promise は completed ハンドラーが返されたときに履行されることを思い出してください。つまり、callback から返される promise は、opPromise1 の結果を最初の項目の completed ハンドラーが処理するまで完了しません。また、join の結果は、元のリストの promise 空の結果を含むオブジェクトによって履行されます。これは、履行値 v には prev プロパティと result プロパティの両方が格納され、result プロパティが opPromise1 の結果であるという意味です。

list の次の項目により、以前の join.then からの promise を含む prev を callback が受け取ります。その後 opPromise1.thenopPromise2 の新しい結合が作成されます。その結果、この join は、opPromise2 が履行され、opPromise1 の completed ハンドラーが返された後に完了します。これによって、この join にアタッチする completed2 ハンドラーが、completed1 が返されるまで呼び出されなくなります。

項目 n の join.then からの promise は completedn が返るまで履行されないという依存関係を、リスト内の各アイテムに構築します。これによって completed ハンドラーが、リストと同じ順序で呼び出されるようになります。

終わりに

この記事では、promise 自体は、多少強力ではあるものの、将来の任意の時点で値を提供するオリジネーターと、これらの値がいつ利用可能になるかを知りたいコンシューマーの間の固有の関係を示す、単なるコード コンストラクターまたは呼び出し規約に過ぎないということを確認しました。このような理由から、非同期操作の結果を示す場合に非常に便利な promise は、JavaScript で記述された Windows ストア アプリで幅広く活用されています。さらに promise の仕様では、連続する非同期操作を連結し、中間結果をリンク間で遷移させることも可能です。

JavaScript 用 Windows ライブラリ (WinJS) が提供する promise の強力な実装を活用することで、あらゆる操作を独自にラップすることができます。さらに WinJS は、並行操作を実現するための promise の結合などの共通シナリオのサポートにも対応しています。これらを活用することで、効率的かつ効果的な非同期操作を WinJS に基づき実行できるようになります。

Kraig Brockschmidt

- Windows エコシステム担当チーム、プログラム マネージャー

著書『HTML、CSS、JavaScript を使った Windows 8 アプリ開発』(英語)