13) AngularJSの$q
JavaScriptのフレームワークであるAngularJS(1系)では $q が提供されています。
$q
AngularJS API Reference
https://docs.angularjs.org/api/ng/service/$q
$q は Q をベースとして実装されたAngularJSのビルトインオブジェクトです。ドキュメントの Differences between Kris Kowal’s Q and $q で触れられているように、ライブラリロードのスピードを重視したコンパクトなライブラリとなっています。
提供される機能
$q で提供される機能は基本的に Q と同じため、メソッドごとの説明は省略します。
コンストラクタ関数
$q(function(resolve,reject){
...
});
resolve/rejectのstaticな呼び出し
$q.resolve(1);
$q.reject('abort');
Deferred
$scope.getDeferred = function(){
var deferred = $q.defer();
deferred.resolve(1);
return deferred.promise;
}
notify
$scope.getDeferred = function(){
var deferred = $q.defer();
$timeout(function(){ deferred.notify('notify'); },500);
$timeout(function(){ deferred.resolve(1); },1000);
return deferred.promise;
}
$scope.getDeferred()
.then(
function(value){ console.info(value); },
null,
function(notify){ console.info(notify); }
)
;
finally
$scope.getDeferred()
.then()
.then()
.finally(function(){
console.info('finally');
})
;
catch
$scope.getDeferred()
.then()
.then()
.catch(function(error){
console.log(error);
})
;
when
$q.when(value)
.then()
.then()
;
all
$.all([taskA(),taskB(),taskC()])
.then(function(values){
console.info(values);
})
;
エラーは同期処理
$q
が他と異なる点は、エラーがPromiseオブジェクトと明確に区別される部分です。
例1) コンストラクタに渡した関数内のエラー
ES6 Promise ではコンストラクタに渡したコールバック関数内でエラーが発生した場合、暗黙のうちに rejected な状態になりますが、$q
はエラーが発生します。
$scope.getPromise = function(){
return $q(function(resolve,reject){
throw new Error('unexpected');
});
}
$scope.getPromise()
.catch(function(error){
console.log(error.message);
})
;
// ブラウザのエラー Error: unexpected
例2) thenチェイン中のエラー
こちらも同じく暗黙のうちに rejected な状態になりません。エラーが発生した後でcatch()
が実行されます。
$scope.getDeferred()
.then(function(){
throw new Error('unexpected');
})
.catch(function(reason){
// 2.ブラウザのエラーの後に実行される
console.log(reason);
})
;
// 1.ブラウザのエラー Error: unexpected
なぜrejectしないのか
AngularJS API Reference $q より引用
From the perspective of dealing with error handling, deferred and promise APIs are to asynchronous programming what try, catch and throw keywords are to synchronous programming.
AngularJSは非同期処理(Promise/Deferred)と、同期処理(try…catch)を明確に住み分けするために、エラーを rejected として扱わないようです。
またこの章のサンプルコードではreject(new Error('abort'))
でなく reject('abort')
のようにテキストを渡しているのに気づいたでしょうか。
公式ドキュメントのサンプルでもreject()
にはテキストが渡されています。 $q ではPromiseオブジェクトとは別にエラーハンドリングを行うのが正しい方法のようです。
Promiseオブジェクトが評価されるタイミング
AngularJSはダイジェストループと呼ばれる仕組みを持っています。ダイジェストループではモデル・式などスコープの持つ値が全て再評価され$watch
やng
イベントが実行されます。逆に捉えると、ダイジェストループが起こるまではスコープには何も起きていません。
AngularJSのビルトインオブジェクトである $q のPromiseオブジェクトも評価のタイミングはダイジェストループです。
ブラウザを通してAngularJSを動作させる場合はDOMの更新やajaxのイベントによりダイジェストループが発生しますが、ユニットテストではそれらがないため自分で$digest()
および$apply()
を使ってダイジェストループを発生させる必要があります。
it('resolved after digest', function() {
var value = null;
$q.resolve(true).then(function(){
value = 1;
});
// まだPromiseが評価されていない
expect(value).toBeNull(null);
// ダイジェストループにより評価される
$scope.$digest();
expect(value).toBe(1);
});