これはあなたの質問に対する完全な答えになることはありませんが、うまくいけば、これが$q
サービスに関するドキュメントを読もうとするときにあなたや他の人を助けるでしょう。それを理解するのにしばらく時間がかかりました。
AngularJSを少し置いて、Facebook API呼び出しについて考えてみましょう。どちらのAPI呼び出しも、コールバックメカニズムを使用して、Facebookからの応答が利用可能になったときに発信者に通知します。
facebook.FB.api('/' + item, function (result) {
if (result.error) {
// handle error
} else {
// handle success
}
});
// program continues while request is pending
...
これは、JavaScriptおよびその他の言語で非同期操作を処理するための標準パターンです。
このパターンの大きな問題の1つは、一連の非同期操作を実行する必要がある場合に発生します。連続する各操作は、前の操作の結果に依存します。それがこのコードが行っていることです:
FB.login(function(response) {
if (response.authResponse) {
FB.api('/me', success);
} else {
fail('User cancelled login or did not fully authorize.');
}
});
最初にログインを試み、次にログインが成功したことを確認した後にのみ、グラフAPIにリクエストを送信します。
この場合、2つの操作をつなぐだけの場合でも、状況は複雑になります。メソッドaskFacebookForAuthentication
は失敗と成功のコールバックを受け入れますが、FB.login
成功したがFB.api
失敗した場合はどうなりますか?このメソッドsuccess
は、FB.api
メソッドの結果に関係なく、常にコールバックを呼び出します。
ここで、各ステップでエラーを適切に処理し、他のユーザーや数週間後にあなたにも判読できるように、3つ以上の非同期操作の堅牢なシーケンスをコーディングしようとしていると想像してください。可能ですが、それらのコールバックをネストし続けるだけで、途中でエラーを追跡するのは非常に簡単です。
ここで、少しの間Facebook APIを脇に置いて、$q
サービスによって実装されているAngular Promises APIについて考えてみましょう。このサービスによって実装されるパターンは、非同期プログラミングを線形の一連の単純なステートメントに似たものに戻す試みであり、方法の任意のステップでエラーを「スロー」し、最後にそれを処理することができ、意味的にはおなじみのtry/catch
ブロック。
この不自然な例を考えてみましょう。2つの関数があり、2番目の関数が最初の関数の結果を消費するとします。
var firstFn = function(param) {
// do something with param
return 'firstResult';
};
var secondFn = function(param) {
// do something with param
return 'secondResult';
};
secondFn(firstFn());
ここで、firstFnとsecondFnの両方が完了するまでに長い時間がかかるため、このシーケンスを非同期で処理したいとします。まずdeferred
、一連の操作を表す新しいオブジェクトを作成します。
var deferred = $q.defer();
var promise = deferred.promise;
promise
プロパティは、チェーンの最終的な結果を示します。promiseを作成した直後にログに記録すると、それが空のオブジェクト({}
)であることがわかります。まだ見るものはありません。
これまでのところ、私たちの約束はチェーンの出発点を表すにすぎません。次に、2つの操作を追加します。
promise = promise.then(firstFn).then(secondFn);
このthen
メソッドはチェーンにステップを追加し、拡張チェーンの最終的な結果を表す新しいプロミスを返します。ステップはいくつでも追加できます。
これまでのところ、一連の関数を設定しましたが、実際には何も起こりませんでした。deferred.resolve
まず、を呼び出して、チェーンの最初の実際のステップに渡す初期値を指定します。
deferred.resolve('initial value');
そして...それでも何も起こりません。モデルの変更が適切に監視されるようにするために、Angularは次の時間$apply
が呼び出されるまで、実際にはチェーンの最初のステップを呼び出しません。
deferred.resolve('initial value');
$rootScope.$apply();
// or
$rootScope.$apply(function() {
deferred.resolve('initial value');
});
では、エラー処理についてはどうでしょうか?これまでは、チェーンの各ステップで成功ハンドラのみを指定してきました。 then
また、オプションの2番目の引数としてエラーハンドラを受け入れます。次に、もう1つの長いプロミスチェーンの例を示します。今回はエラー処理を行います。
var firstFn = function(param) {
// do something with param
if (param == 'bad value') {
return $q.reject('invalid value');
} else {
return 'firstResult';
}
};
var secondFn = function(param) {
// do something with param
if (param == 'bad value') {
return $q.reject('invalid value');
} else {
return 'secondResult';
}
};
var thirdFn = function(param) {
// do something with param
return 'thirdResult';
};
var errorFn = function(message) {
// handle error
};
var deferred = $q.defer();
var promise = deferred.promise.then(firstFn).then(secondFn).then(thirdFn, errorFn);
この例でわかるように、チェーン内の各ハンドラーは、トラフィックを次の成功ハンドラーではなく次のエラーハンドラーに転送する機会があります。ほとんどの場合、チェーンの最後に単一のエラーハンドラーを配置できますが、回復を試みる中間エラーハンドラーを配置することもできます。
あなたの例(そしてあなたの質問)にすばやく戻るために、これらは、Facebookのコールバック指向のAPIをAngularのモデルの変化を観察する方法に適応させる2つの異なる方法を表しているとだけ言っておきます。最初の例では、API呼び出しをプロミスにラップします。これはスコープに追加でき、Angularのテンプレートシステムによって認識されます。2つ目は、コールバックの結果をスコープに直接設定し、$scope.$digest()
Angularに外部ソースからの変更を認識させるために呼び出す、よりブルートフォースなアプローチを採用しています。
最初の例にはログイン手順がないため、2つの例は直接比較できません。ただし、一般に、このような外部APIとの対話を個別のサービスにカプセル化し、約束どおりに結果をコントローラーに配信することが望ましいです。そうすることで、コントローラーを外部の問題から分離し、モックサービスでより簡単にテストできます。