ボクココ

個人開発に関するテックブログ

さよならコールバック地獄. 注目のPromise Pattern とは

非同期プログラミングをしていると、俗にいうコールバック地獄に悩まされることになる。 何かの処理が終わったらこれをやって、その次にこれをやって・・みたいな処理は深くネストされたコールバックを延々と書くことになるだろう。 これを解決してくれるのがPromise. 主流のJavaScript ライブラリで搭載されるようになり、その他の言語でも急速に普及され始めている。 今回はそれをAngularJSを例にして大まかにまとめてみた。

Promise API とは

従来の非同期プログラミングは非同期の性質から複雑になりがちだった。 特に非同期のイベントが複数発生した際の同期処理など。

同期の世界ではメソッドチェインによる呼び出しとエラー処理が直感的にかける。

Promise API は非同期プログラミングをより簡単にするためのもの。 AngularJSでは$q Service としてPromise APIを提供している。 これを利用することで、非同期処理をまるで同期処理のメソッドチェインのように記述することができるようになる。

まずはコードサンプル

var Person = function(name, $log) {
  this.eat = function(food) {
    $log.info(name + “ は “ + food “を食べます”;
  };
  this.beHungry = function(reason) {
    $log.warn(name + “ は “ + reason + “で空腹です。”;
  }
};
var Restaurant = function($q, $rootScope) {
  var currentOrder;

  this.takeOrder = function(orderedItems) {
    currentOrder = {
      deferred :  $q.defer(),
      items      :  orderedItems
    };
  return currentOrder.deferred.promise;
  };

  this.deliverOrder = function() {
    currentOrder.deferred.resolve(currentOrder.items);
    $rootScope.digest();
  };
  
  this.failOrder = function(reason) {
    currentOrder.deferred.reject(reson);
    $rootScope.digest();
  };
};

$q.defer() で deferredオブジェクトを生成。 これは今後の処理が成功するか失敗するかを表現するオブジェクト。 deferredオブジェクトには2つの役割がある:

  • promise オブジェクトを持つ。 これはdeferredタスクの未来の結果
  • 未来の処理を発生させる resolve(成功) と reject(失敗) メソッドを持つ。

$rootScope.digest() でAngularJSへpromise の解決の伝搬を行う

実際に使うところ

var taro      = new Person(‘太郎’, $log);
var hanako = new Person(‘花子’, $log);
var sushi = new Restaurant($q, $rootScope);

var sushiDelivered = sushi.takeOrder(‘マグロ’);

sushiDelivered.then(taro.eat, taro.beHungry);
sushiDelivered.then(hanako.eat, hanako.beHungry);

sushiDelivered.deliverOrder();  
//-> 太郎 は マグロを食べます 
//-> 花子 は マグロを食べます 

then(resolved, failed) の順。 failed はなくてもOK. then で登録されたすべてのコールバックがpromise の解決により実行される 一度resolve か reject したら後でその結果はかえられない。

promise の本当に力が発揮されるのは非同期の世界を同期的に実行するかのように振る舞わせるとき。

var grill = function(sushi) {
  return “焼いた “ + sushi;
};
sushi.takeOrder(‘マグロ’).then(grill).then(taro.eat, taro.hungry);

エラー時のコールバックの工夫

エラーコールバックはcatch ブロックのような振る舞いをする。 これを利用して以下のことができたりする

  • 復帰 (catchブロックから値を返却する)
  • エラーの伝搬(例外を再び throw する)
var retry = function(reason) {
  return sushi.takeOrder(‘マグロ’).then(grill);
};
sushi.takeOrder(‘マグロ’).then(grill, retry).then(taro.eat, taro.beHungry);

sushi.failOrder(‘品切れ’);
sushi.deliverOrder();
// -> 太郎は 焼いたマグロを食べます

最終的に利用者は途中で何か問題が起こったことを知らない

例外の再throw

resonに説明文言を追加する例
var explain = function(reason) {
  return $q.reject(‘注文された寿司は品切れです’);
};

sushi.takeOrder(‘エビ’).then(grill, explain).then(taro.eat, taro.beHungry);

$q.all

複数の非同期タスクをスタートすることと、全てのタスクが完了した際にだけ通知されることを可能にする. いくつかの非同期アクションがあり、合流地点として一つの結合されたpromise を返却する。

var ordersDelivered = $q.all([
  sushi.takeOrder(‘マグロ’);
  salad.takeOrder(‘サラダ’);
]);
ordersDelivered.then(taro.eat);
sushi.deliverOrder();
salad.deliverOrder();
// -> 太郎は マグロ,サラダを食べます

すべてのpromiseがresolvedされたときだけresolveと見なす 一つの個別アクションが失敗した際はpromiseはrejectされる。

$q.when

同期処理と非同期処理アクションから得た結果を処理したい場合がある。 この場合、すべての結果を非同期の結果として扱うと簡単になる サラダは準備できた(同期)けど、寿司は注文と配達が必要(非同期)な場合

var ordersDelivered = $q.all([
  sushi.takeOrder(‘マグロ’);
  $q.when(‘自家製サラダ’);
]);

ordersDelivered.then(taro.eat, taro.beHungry);

sushi.deliverOrder();
// -> 太郎 はマグロ,自家製サラダを食べます

$q.whenは与えられた引数で解決されたpromiseを返却する。

簡単なまとめ

var deferred;
var init = function() {
  deferred = $q.defer();
  return deferred.promise;
}; 

でまず promise オブジェクトを返す。

init().then(success, error);

でそれぞれのsuccess, error コールバック処理を記述

// 何らかの非同期処理
deferred.resolve(param);

でコールバックを通知


上記サンプルコードは 下記の本を一部引用させていただきました。

Mastering Web Application Development with AngularJS

Mastering Web Application Development with AngularJS

より詳細はこちらの本で! その他 AngularJS のトピックもかなり詳細に書かれていておすすめです。※英語