目次へ戻る


13) AngularJSの$q

JavaScriptのフレームワークであるAngularJS(1系)では $q が提供されています。

$q

AngularJS API Reference

https://docs.angularjs.org/api/ng/service/$q

$qQ をベースとして実装されたAngularJSのビルトインオブジェクトです。ドキュメントの Differences between Kris Kowal’s Q and $q で触れられているように、ライブラリロードのスピードを重視したコンパクトなライブラリとなっています。

提供される機能

$q で提供される機能は基本的に Q と同じため、メソッドごとの説明は省略します。

コンストラクタ関数

$q(function(resolve,reject){
  ...
});

plunker

resolve/rejectのstaticな呼び出し

$q.resolve(1);
$q.reject('abort');

plunker

Deferred

$scope.getDeferred = function(){
  var deferred = $q.defer();
  deferred.resolve(1);
  return deferred.promise;
}

plunker

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); }
  )
;

plunker

finally

$scope.getDeferred()
  .then()
  .then()
  .finally(function(){
    console.info('finally');
  })
;

plunker

catch

$scope.getDeferred()
  .then()
  .then()
  .catch(function(error){
    console.log(error);
  })
;

plunker

when

$q.when(value)
  .then()
  .then()
;

plunker

all

$.all([taskA(),taskB(),taskC()])
  .then(function(values){
    console.info(values);
  })
;

plunker

エラーは同期処理

$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

plunker

例2) thenチェイン中のエラー

こちらも同じく暗黙のうちに rejected な状態になりません。エラーが発生した後でcatch()が実行されます。

$scope.getDeferred()
  .then(function(){
    throw new Error('unexpected');
  })
  .catch(function(reason){
    // 2.ブラウザのエラーの後に実行される
    console.log(reason);
  })
;

// 1.ブラウザのエラー Error: unexpected

punker

なぜ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はダイジェストループと呼ばれる仕組みを持っています。ダイジェストループではモデル・式などスコープの持つ値が全て再評価され$watchngイベントが実行されます。逆に捉えると、ダイジェストループが起こるまではスコープには何も起きていません。

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);
});


前のページ