m.deferred



これは、Mithrilの低レベルメソッドです。これはthenable APIのカスタム版です。

deferredは非同時性モナドです。deferredは、コールバックをバインドして、グラフ構造の計算木を構築するのに使えるpromiseプロパティを公開しています。

deferredは、resolveもしくはrejectを呼び出すことで、値を適用することができます。その値は計算木に渡って処理されていきます。

それぞれの計算関数はその値を引数として受け取り、他の値を返すことが期待されています。その値は、木の中の次の計算関数に渡されていきます。

m.deferred関数が返すdeferredオブジェクトは、resolverejectの2つのメソッドと、promiseと呼ばれるプロパティを持ちます。これらのメソッドを呼ぶと、promise木に値を送信することができます。promiseプロパティは、promise木のルートです。このプロパティは、successCallbackコールバックと、errorCallbackコールバックを受け取る、thenメソッドを持っています。thenメソッドを呼ぶと、 successCallbackと、 errorCallbackで表される計算表現をpromiseに追加します。これは、resolverejectが呼ばれると呼び出されます。thenメソッドは子供のpromiseを返します。この子のpromiseにもさらに子のpromiseを追加することができます。

promiseは普通の関数です。具体的にはm.prop getter-setterで、promiseが成功すると、successCallbackが返す値が格納されます。

Mithrilのプロミスは自動的に自動再描画システムと統合されることはありません。もし、jQuery.ajax)などのサードパーティの非同期ライブラリと一緒に使って、リクエストの完了時に再描画をさせたい時はm.startComputation / m.endComputationを使用してください。


使用方法

//単体での利用
var greetAsync = function() {
    var deferred = m.deferred();
    setTimeout(function() {
        deferred.resolve("hello");
    }, 1000);
    return deferred.promise;
};

greetAsync()
    .then(function(value) {return value + " world"})
    .then(function(value) {console.log(value)}); //1秒後に"hello world"と表示

getter-setter APIを使ったデータの取得

promiseオブジェクトは実際のgetter-setter関数であるため、promiseが果たされると、値が格納されます。

//非同期サービス
var greetAsync = function() {
    var deferred = m.deferred();
    setTimeout(function() {
        deferred.resolve("hello");
    }, 1000);
    return deferred.promise;
};

//非同期の消費者
var greeting = greetAsync()
var processed = greeting.then(function(value) {return value + " world"})

console.log(greeting()) // undefined - `deferred.resolve`がまだ呼ばれてない

setTimeout(function() {
    //ここで `deferred.resolve` が呼ばれた
    console.log(greeting()) // "hello"
    console.log(processed()) // "hello world"
}, 2000)

Mithrilの再描画システムへの統合

デフォルトでは、promiseはMithrilの自動再描画システムとは統合しません。非同期関数を使用して、その非同期で取得してきた値をビューに反映させたい時は、[m.startComputation / m.endComputation]を呼ぶ必要があります。

//非同期サービス
var greetAsync = function() {
    //Mithrilにサービス呼び出しが終わるまで再描画を待つように伝える
    m.startComputation();

    var deferred = m.deferred();
    setTimeout(function() {
        deferred.resolve("hello");

        //サービスが完了したら、Mithrilに再描画するように伝える
        m.endComputation();
    }, 1000);
    return deferred.promise;
};

非同期のコールバックを待たずに再描画を行っても問題ないケースもあります。その場合は、単にm.startComputation/m.endComputation呼び出しを省略します。

再描画の操作によっては、呼び出し前と後の両方に再描画が必要になるでしょう。この場合は、m.startComputation/m.endComputationを呼ぶ代わりに、m.redrawを呼び出してください。

//非同期サービス
var greetAsync = function() {
    //サービスを待たずに即座に再描画する

    var deferred = m.deferred();
    setTimeout(function() {
        deferred.resolve("hello");

        //再び再描画する
        m.redraw()
    }, 1000);
    return deferred.promise;
};

Promises/A+との違い

ほとんどの部分で、MithrilのpromiseはPromise/A+の仕様通りの動作をしますが1つ異なる点があります。Mithrilのpromiseは、可能であれば同期で処理を行います。

同期実行

可能であればMithrilのpromiseは同期実行しようとします。下記のコードを使用して、MithrilとA+ promisesの違いを紹介します:

var deferred = m.deferred()

deferred.promise.then(function() {
    console.log(1)
})

deferred.resolve("value")

console.log(2)

このサンプルを実行すると、A+ promisesの場合は、 1の前に2をログ出力しますが、Mithrilは2の前に1を表示します。一般的には、resolve/rejectthenメソッドが呼ばれた後に非同期で呼ばれます。一般的にはこの差はそれほど大きな問題ではありません。

Mithrilがコールバックを同期で実行しているにはいくつか理由があります。仕様を満たすには、setImmediateポリフィル(将来のバージョンのJavaScriptと互換性を持たせるための、非常に巨大なライブラリ)か、setTimeout(仕様により、最低でも4ミリ秒の待ちが発生する)が要求されます。Mithrilは敏捷性とパフォーマンスにフォーカスしているため、これらのトレードオフを受け入れることはできませんでした。

チェックしていないエラーのハンドリング

デフォルトでは、MithrilはエラーがErrorクラスのサブクラスであれば、必ずフレームワーク外までそれを届けます。Promises/A+の仕様通りであれば、例外クラスそのもののインスタンス、もしくは何らかのプリミティブなオブジェクトを手動で投げるとrejectionコールバックが呼ばれます。

仕様と異なるこの動作により、タイプミスによるnull参照例外などの一般的なエラーを開発者が簡単に見つけることができます。デフォルトでは、Promise/A+の仕様ではすべての例外はrejectionを起動しますが、開発者がこの失敗時のコールバックを設定し忘れると、誰もエラーを受け取らずに例外は握りつぶされます。

開発者は、最初にその存在を把握しなければ、nullになる可能性のあるオブジェクトのプロパティのアクセスエラーのハンドリングをしたいとは思わないでしょう。このような問題を防ぐ唯一の方法は、null参照例外が発生する可能性のあるすべての箇所に存在チェックを追加する方法しかありません。このようなエラーに対しては、例外が発生したときにエラーメッセージと行番号をコンソールに表示する方が開発者にやさしいでしょう。

m.request({method: "GET", url: "/things"})
    .then(function(items) {
        item.foreach(doSomething) //プログラマのエラー: タイプミスによって実行時エラーがログに出力
    })

もちろん、コインの反対側の標準のPromise/A+の仕様もサポートされています。バリデーションの失敗があってエラーメッセージを表示しなければならない場合など、開発者がpromiseのコールバックの中で例外の発生を知らせなければならない場合は、new Errorで作成した例外を投げて、それを受け取ることができます。

var error = m.prop()
m.request({method: "GET", url: "/user/:id", data: {id: 1}})
    .then(function(user) {
        if (!user.isAdmin) throw new Error("もうしわけありません。権限がありません。")
    })
    .then(null, error) //アプリケーションのエラーをテンプレートで表示するためにsetter-getterにバインド

このように、デフォルトの例外処理のセマンティクスが変更されています。次のセクションもご覧ください。


例外モニタ

promiseコールバックの中で例外が発生すると、Mithrilはm.deferred.onerror(e)を呼びます。

デフォルトでは、このイベントハンドラはこのエラーがErrorのサブクラスの例外で、Errorそのもののインスタンスでなければコンソールにその例外を出力します。そうでない場合は、Promises/A+の仕様に従います。Mithrilがこのような動作をするのは、多くのプログラマにとって、null参照例外などの未知の例外はデバッグのためにコンソールに出力される方がうれしいからです。

言い換えると、JavaScript開発者は例外に関して、アプリケーションのエラーハンドリングを目的としてErrorのサブクラスを作って投げることはほとんどありません。Errorのプロトタイプチェーンを気にすることはほとんどありません。

もし標準のエラーをモニタリングする仕組みが期待に合わなければ、onerrorを安全に置き換えて使用できます。

//すべてのエラーを無視する
m.deferred.onerror = function() {}

//エラーを単にログに出力する
m.deferred.onerror = function(e) {console.error(e)}

シグニチャ

シグニチャの読み方

Deferred deferred() {void onerror(Error e)}

where:
    Deferred :: Object { Promise promise, void resolve(any value), void reject(any value) }
    Promise :: GetterSetter { Promise then(any successCallback(any value), any errorCallback(any value)), Promise catch(any errorCallback(any value)) }
    GetterSetter :: any getterSetter([any value])
  • GetterSetter { Promise then([any successCallback(any value) [, any errorCallback(any value)]]) } promise

    promiseは、2つの計算のコールバックを持つthenメソッドを持っています。

    thenメソッドは親のpromiseの計算結果を受け取る、別のpromiseを作って返します。

    promiseはgetter-setter (m.prop参照)。resolverejectのどちらを呼んでも、promiseは親の計算結果の値 (もしくはpromiseが親を持っていなければresolve/reject)を保持します。

    Promiseのcatchメソッド呼び出しは、then(null, errorCallback)と等価です。

    • Promise then([any successCallback(any value) [, any errorCallback(any value)]])

      このメソッドは、resolverejectメソッドに渡される値を受け取り、結果のpromiseに格納される値を処理して返す2つのコールバックを受け取ります。

      • any successCallback(any value) (optional)

        ルートのdeferredresolveが呼ばれるとsuccessCallbackが呼ばれます。

        この設定がfalsyな値だったときのデフォルトの動作は、function(value) {return value}という関数と等価です。

        もしこの関数がundefinedを返してきたら、値がどんなでものであろうと、value引数がthenableキューの次のキューに渡されます。

      • any errorCallback(any value) (optional)

        ルートのdeferredrejectが呼ばれるとerrorCallbackが呼ばれます。

        この設定がfalsyな値だったときのデフォルトの動作は、function(value) {return value}という関数と等価です。

        もしこの関数がundefinedを返してきたら、値がどんなでものであろうと、value引数がthenableキューの次のキューに渡されます。

      • returns Promise promise

  • void resolve(any value)

    このメソッドは、deferredオブジェクトの子のpromiseのsuccessCallbackに値を渡します。

  • void reject(any value)

    このメソッドは、deferredオブジェクトの子のpromiseのerrorCallbackに値を渡します。

  • m.deferred.onerror

    void onerror(Error e)

    このメソッドはpromiseコールバックの中で例外が発生するたびに呼ばれます。デフォルトでは、このイベントハンドラはこのエラーがErrorのサブクラスの例外で、Errorそのもののインスタンスでなければコンソールにその例外を出力します。そうでない場合は、Promises/A+の仕様に従います。