読者です 読者をやめる 読者になる 読者になる

Promise時代のJavaScriptの関数の処理/提供

最近自分で非同期前提のプラグイン書くときはThenableな感じで書いてることが多い。 Thenableってのはどういうことかというと、typescirptのes6-promises では次のように定義してある。

interface Thenable<R> {
    then<U>(onFulfilled: (value: R) => Thenable<U>,  onRejected: (error: any) => Thenable<U>): Thenable<U>;
    then<U>(onFulfilled: (value: R) => Thenable<U>, onRejected?: (error: any) => U): Thenable<U>;
    then<U>(onFulfilled: (value: R) => U, onRejected: (error: any) => Thenable<U>): Thenable<U>;
    then<U>(onFulfilled?: (value: R) => U, onRejected?: (error: any) => U): Thenable<U>;  
}

Thenableな設計とは、「なんかの処理結果がthen関数を持ってたらそれを呼ぶ。なかったら知らん」みたいな感じ。thenがあったらコールバックを預ける、というイディオムは、jQuery.DeferredにもPromiseにも適用できる。相手がどっちか知らなくても良いので覚えておいて損はない。

相手がPromiseであると期待する

ここにいかにも非同期なfetchItem関数があるとする。Promise時代の設計ならば、それはきっとPromiseオブジェクトを返すはずだ!と思うのは自然。

var promise = fetchItem();
if(promise != null && promise.then instanceof Function) promise.then(function(){ console.log('fetchItem done!'); })

(ここではpromiseという変数にしたけど、Promiseオブジェクトを進行形で表現する派閥もあった。fetching.then... みたいな)

追記: 1399131306128

次のcontinueAnywayはPromise.resolve() 使えば不要。thx @azu_re @Constellation

追記終わり。

ちなみにmizchi/wardenでは次のようなヘルパを用意して、「実行結果がpromiseだったらコールバックを預け、違ったらその場で実行する」みたいな感じにしていた。<>

https://github.com/mizchi/warden/blob/master/src/warden.coffee#L124

coffeescript

continueAnyway = (maybePromise, next) -> maybePromise?.then?(next) ? next()

javascript(コンパイルしただけ)

var continueAnyway = function (maybePromise, next) {
  var _ref;
  return (_ref = maybePromise != null ? typeof maybePromise.then === "function" ? maybePromise.then(next) : void 0 : void 0) != null ? _ref : next();
}

まあこれはcoffeescriptの出力するコードがおぞましい感じになるんだけど、coffeescript?. が便利ですねといったところ

非同期関数を提供する

自分でPromise化された関数を定義する場合は、冒頭で return new Promise してしまうのが見通しが良いと思っていて、いろんなところで提案してる。

fetchItem = -> new Promise (done) => $.get('/item/foo').then done
var fetchItem = function(){
  return new Promise(function(done, reject){
    $.get('/items/foo').then(done, reject);
  });
}
fetchItem().then(function(data){ console.log(data)});

まだ標準でPromiseがあるかどうかは微妙な時代なので、とりあえず jakearchibald/es6-promise https://github.com/jakearchibald/es6-promise をいれる。bower/npm で install es6-promise すればいいと思う。開発者ならChrome使ってると思うけどChrome34でPromise入ったので他のブラウザにないことを忘れがちになりそう。

Promiseの目的と設計

Promiseの実践上の最も大きな目的は、複数の非同期処理をPromise.all で「並列で走らせるか」、thenでパイプして「直列で走らせるか」ユーザー側で切り替えることができるようにすることにある。

その相手はネットワークでなくてもよく、例えばユーザーとのインタラクションを次のように定義することもできる。

(ここからfunction多用で非常にだるいのでcoffeeで書く。このだるさはES6のarrow functionで解決する。コード例で僕がcoffee好きな理由を察してほしい)

# ユーザーの操作を待つことをPromise化した関数
waitUserInput = -> new Promise (done) ->
  $('button.js-item-id').off().on 'click', (e) =>
    # .. なにか処理をする
    done(selected: 'itemA')

# アイテムIDを引数に詳細情報を取得する関数
fetchItemDetail = (itemId) -> new Promise (done) ->
  $.get('/items',{id: itemId}).then (data) => done(data)

# ユーザーがアイテムを選択したらその情報を取得してconsole.logに流す
waitUserInput()
  .then (userInput) => fetchItemDetail(userInput.selected)
  .then (data)      => console.log(data)

このとき、fetchItemInfoで複数のアイテムの情報をとりたいときは、並列でリクエストする用に処理を組める

Promise.all([
  fetchItemDetail('a')
  fetchItemDetail('b')
  fetchItemDetail('c')
]).then ([a, b, c]) => console.log(a, b, c) # coffeescript の destractive assingment

もちろん、根本の設計として並列リクエストを多用すると貧弱なモバイル端末が不安定になることがあるので、30並列リクエストとかになりそうならそれ用のAPIを作ったほうがよい。

命名ルール

getは同期、fetchは非同期なのは暗黙の了解っぽい。requestやsync等, ネットワークに関わる同志はとりあえず非同期というパターンが多い。MSっぽい命名だったら最後にSyncとかAsyncとかつけたりする。最近はTypeScriptのせいかそれなりにMSのプレゼンスがある。

さらに次の時代へ

ちなみにPromise時代の次にはyieldによるGenerator時代が控えている。備えよう。