Deferredのコールバックスタイルと比較したメリット/デメリット

まだそんなに使い込んでいるわけではないけれど。
使う前はコールバックスタイルに比べてなにか利点あるの?別にコールバックスタイルで困らないんじゃない?って思っていたのでまとめてみる。

メリット

  • コールバック引数が消えてすっきりする
  • 処理1をやって非同期処理を挟んで、処理2をやって、処理3をやって...というときにコールバックスタイルだと汚くなるのが綺麗にかける
  • 非同期処理でもエラーハンドリングが綺麗にできる
  • (一部のDeferredライブラリのみ) 非同期処理を行っている最中にキャンセルできる。ちょうどスレッドの使える言語で、バックグラウンドで処理を行っているスレッドをとめるように
  • ライブラリにもよるが以下のようなことをするためのユーティリティがある。コールバックスタイルでもそういうユーティリティは作れるっちゃ作れるが、あらかじめ存在するDeferredライブラリに任せちゃう方がいいよね
    • 複数の非同期処理を一斉に実行して全部揃ったら次を実行する (JSDeferredのparallel)
    • 非同期処理をはさむ処理を任意回数順番にやる (JSDeferredのloop)

デメリット

  • Deferredを理解しないと書けない・読めない
    • 僕の場合、Deferredライブラリで何をやっているのか実装を読まないと不安で使えなかったので、僕みたいな人はDeferredライブラリの実装を読む手間もある
  • ライブラリにもよるが、Deferredを使った場合、何もしていないと非同期処理内で起こったエラーが握りつぶされてしまう
    • エラー用のコールバックを追加して、それを表示するとか、setTimeoutして再スローするとかしないといけない
    • ちなみにMochiKitのDeferredが元になっているclosure-libraryのDeferredでは捕捉されなかったエラーは再スローされるように改良されている。素晴らしい

処理1をやって非同期処理を挟んで、処理2をやって、処理3をやって...というときにコールバックスタイルだと汚くなるのが綺麗にかける

コールバックスタイルで普通に書くとこういうコールバックのネストになる

function (cont) {
  処理1;
  someAsyncProcess(..., function () {
    処理2;
    someAsyncProcess(..., function () {
      処理3;
      cont();
    });
  });
}

関数に名前をつけてフラットにすることもできるが... この方法は東京Node学園#1「非同期プログラミングの改善」のエッセンスでは「名前でジャンプするなんてgoto文でしょ」と言われている。
個人的にはそんな悪くないと思うけど、書くのがだるいのは確か

function (cont) {
  phase1();
  function phase1() {
    処理1;
    someAsyncProcess(..., phase2);
  }
  function phase2() {
    処理2;
    someAsyncProcess(..., phase3);
  }
  function phase3() {
    処理3;
    cont();
  }
}

Deferred (ここではJSDeferred) を使うとこう

// someAsyncProcessはここでは上と違って
// コールバック引数を受け取る関数ではなくDeferredを返す関数とする

function () {
  return next(function () {
    処理1;
    return someAsyncProcess(...);
  }).
  next(function () {
    処理2;
    return someAsyncProcess(...);
  }).
  next(function () {
    処理3;
  });
}

非同期処理でもエラーハンドリングが綺麗にできる

正直、非同期処理でエラーハンドリングしたいと思ったことがないのであんまり...

(一部のDeferredライブラリのみ) 非同期処理を行っている最中にキャンセルできる

これはJSDeferredにはない機能。MochiKitのDeferredで可能。これのためにDeferredを使い始めた。

// 0.1秒間隔で0, 1, 2, 3, 4, 5 .. と永遠に出力する
function f() {
  return loop(0);
  function loop(i) {
    console.log(i);
    return MochiKit.Async.callLater(0.1, loop, i + 1);
  }
}

var deferred = f();

// 一秒後に処理を中断する
// (ボタンが押されたら処理を中断、のように変えてももちろんいい)
setTimeout(function () {
  deferred.cancel();
}, 1000);