jQuery.Deferredの使いどころ

便利便利と言われつつも使ってみないと良さがわからないのがDeferred Object。身近なところで言うと、$.ajaxが返す、doneとかfailとかを呼べるアレもDeferred Objectです。使いこなすと色々な処理をコールバックを渡すよりもうちょっと綺麗に書けるようになります。

ということで最近プロジェクトで使ったパターンを中心に例をあげてみます。

コードはすべてCoffeeScript擬似コードです。

まず最初に便利な書き方を覚える

Deferred Objectは$.Deferred()で作るわけですが、これには「作られたDeferred Object自体を引数にとる関数」を引数として渡せます。これを利用すると

d = $.Deferred()
doSomethingWithCallback -> d.resolve()
d

$.Deferred (d) -> doSomethingWithCallback d.resolve

と書けます。一行で、コールバックを取る関数をDeferred Objectを返す関数にすることができるわけです。これは僕が働いているQuipperでもイディオムになってて、コード量削減に寄与しています。引数についての詳細はこちら を。

複数イベントにまたがる一連の処理を逐次実行っぽく書く

こんな具合にイベント間に依存関係が生まれる事ってありますよね。 フラグで実行順序を制御しているのが切ないです。

list.on 'selected', ->
  uploadSelection().done ->
    uploaded = true

input.on 'entered', ->
  if uploaded
    uploadEnteredContent().done ->
      entered = true

confirmation.on 'confirmed' ->
  if uploaded and entered
    uploadConfirmed().done ->
      confirmed = true

goToNextPage.on 'click', ->
  if uploaded and entered and confirmed
    route "/next_page"

実際のアプリケーションを書くと、イベントハンドラ内はもっとゴチャゴチャするはずです。

これはjQuery.Deferredを使えば、 こう書けます。thenにより逐次実行っぽい書き方ができるので、フラグによる実行順序の制御は不要です。

select = $.Deferred (d) -> list.on 'selected', ->
  uploadSelection().then d.resolve

enter = $.Deferred (d) -> input.on 'entered', ->
  uploadEnteredContent().then d.resolve

confirm = $.Deferred (d) -> confirmation.on 'confirmed', ->
  uploadConfirmed().then d.resolve

$.Deferred().resolve()
  .then(select)
  .then(enter)
  .then(confirm)
  .then -> routeTo "next page"

沢山の非同期処理をまとめる

例えば、画像の先読み

preload = (sources) ->
  promises = _.map sources, (src) ->
    d = $.Deferred()
    img = new Image
    img.src = src
    img.onload = d.resolve
    img.onerror = d.reject
    d.promise()

  $.when(promises...)

preload [
  "assets/foo.png"
  "assets/bar.png"
  "assets/baz.png"
]

(Thanks @mizchi)

例えば複数のAjax Request

a = $.get("https://somewhere.com/api/a")
b = $.get("https://somewhere.com/api/b")
c = $.get("https://somewhere.com/api/c")

$.when(a,b,c).then (aResponse, bResponse, cResponse) ->
  new AView model: aResponse.responseJSON
  new BView model: bResponse.responseJSON
  new CView model: cResponse.responseJSON

ところでPromise/A+って何?ES6 Promisesとか何なの?jQuery.Deferredで大丈夫なの?

まず、Promiseというのはいわゆるデザインパターンのひとつ。Wikipediaによると、用語自体はSchemeに由来しているようです。Promise/AというのはJavaScriptにおけるPromiseの標準仕様のProposal。それの強化版としてPromise/A+がある。jQuery.DeferredはPromise/Aにざっくり準拠。ES6 PromisesはPromise/A+にだいたい準拠。つまり、どれもだいたい同じAPIと挙動を持っています。僕はプロジェクトでjQuery.DeferredからQへの乗り換えを試みてみましたが、あまり変わらなかったのでjQuery.Deferredに戻りました。 ということで、とりあえずjQuery.Deferredでも大丈夫だと思います。ただ例外の扱い、doneの挙動などが違うので気をつけて。

余談ですがPromiseをモナドにするって提案をJavaScriptの人が「現実を完全に無視した型付き言語のおとぎの国の話しだろ」って無碍に却下するという事件がありました。 個人的にはPromise/Aの 1) 成功/失敗、 2) 非同期処理の制御フロー という2つの要素をごっちゃにしてるのが汚いなあと思ってて、Brian McKennaさんの提案する仕様のほうがクリーンかつ一貫性があって、いいと思うんですけどね。