自動再描画システム

Mithrilは、データは常にモデルからビューに流れる、という原則のもとに設計されています。これにより、UIの状態や状態のテストが簡単に行えるようになっています。この原則をきちんと実装するためには、データとの同期が漏れているUI部品がなくならないようにするために、再描画のアルゴリズムをビュー全体に対して漏れ無く実行する必要があります。初めて見るとデータ変更があるたびに全体を再描画するのは高コストに見えると思いますが、変更のあるDOMだけを更新するという高速な差分アルゴリズムを使い、Mithrilでは効率的にこれを行っています。DOMはレンダリングエンジンにとって最大のボトルネックであるため、MithrilのDOMの仮想表現に対して差分をとり、必要に応じてバッチで実際のDOMを変更するアプローチは、驚くべきパフォーマンスを達成しています。

これに加えて、Mithrilはアプリケーションのライフサイクルを考え、適切なタイミングでのみ再描画を実行するようにしています。ほとんどのフレームワークでは、再描画を積極的に行いますが、やりすぎてしまうという間違いをおかしています。というのも、なるべく効率よくやろうとすると、再描画を行うベストなタイミングを決定するのはとても難しい問題だからです。

Mithrilは、再描画をいつどんな戦略で行うかを決定するメカニズムをいくつか提供しています。デフォルトでは、コンポーネントのコントローラが初期化されるとゼロから再描画を行う設定になっていて、イベントハンドラが起動されると差分描画が行われるように設定されます。これに加えて、Mithril外の非同期のコールバックからも、m.startComputation関数とm.endComputation関数を適切な場所に配置することで(あとで説明します)、再描画のシステムを起動することができます。m.startComputation呼び出しと、対応するm.endComputation呼び出しにあるコードは、Mithrilの文脈では、「同じペアの関数呼び出しのコンテキスト内に存在する」と表現します。

m.requestを呼び出したり、m.startComputationm.endComputationのコンテキストをネストすることで、 再描画を遅らせることができます。再描画エンジンは、内部のカウンタを使って再描画のタイミングを遅らせます。カウンタはm.startComputationを呼び出すとインクリメントされ、m.endComputationを呼び出すとデクリメントされます。カウンタがゼロになるとMithrilは再描画を行います。この関数呼び出しのペアを戦略的に配置することで、アプリケーションコード全体で状態変数を管理する必要性をなくし、データを取得する非同期のサービスをいくつもスタックさせることが可能になっています。これによって、m.requestや他のデータサービスとの統合をシームレスに行えるようになっています。Mithrilはすべての非同期操作が完了するのをまってから再描画を行います。

このように、再描画の決定にデータの可用性を判断基準にしていますが、これ以外にも、ブラウザの可用性についても考慮しています。もし、短時間で何度も再描画が行われようとしている場合には、Mithrilはこれらの再描画をまとめて、単一のアニメーションフレーム(16ミリ秒)に最大1回のみ再描画します。コンピュータのスクリーンはフレーム以上の速度で表示することはできないため、この最適化によってCPUのサイクルを節約することができますし、大量のデータ変更に対してもUIの応答性を保つことができます。

Mithrilはより深いレベルでエンジンの再描画の動作を制御できるように、いくつかのフックを提供しています。m.startComputationm.endComputationは再描画のコンテキストを作成します。m.redrawを呼び出すと次のフレーム更新時に強制的に再描画を行います。また、オプションで同期処理として再描画を行わせることもできます。configのretainフラグを使うと、ラウト変更時に特定の要素を再描画するかどうかを指定できます。m.redraw.strategyを使うと、Mithrilが次に再描画を計画する方法を変更できます。また、開発者が再描画の仕組みを完全に止める選択をした場合にも、m.render関数を使うことが出来ます。


自動再描画システムの統合

MithrilのAPIを使わないで、カスタムの非同期呼び出しを行った時に、ビューが更新されていない時は、m.startComputation / m.endComputationを使用することを検討してください。カスタムコードの処理が終わった時にMithrilが賢く自動再描画を行います。

非同期のコードとMithrilの自動再描画システムを統合する時は、非同期処理を呼び出す「前に」m.startComputationを呼び出し、非同期のコールバックの最後でm.endComputationを呼び出してください。

//このサービスは1秒間待ってログに"hello"と出力し、その後ビューに
//再描画を行うように知らせています (他の非同期処理により延期されないかぎり)
var doStuff = function() {
    m.startComputation(); //`startComputation`は非同期の`setTimeout`の前に呼び出す

    setTimeout(function() {
        console.log("hello");

        m.endComputation(); //`endComputation`はコールバックの最後で呼び出す
    }, 1000);
};

同期処理のコードと統合する場合は、メソッドの先頭でm.startComputationを呼んで、最後にm.endComputationを呼んでください。

window.onfocus = function() {
    m.startComputation(); //イベントハンドラの先頭で、他の処理よりも先に呼ぶ

    doStuff();

    m.endComputation(); //イベントハンドラの最後で、他の処理よりも後に呼ぶ
}

ライブラリ内のそれぞれのm.startComputation呼び出しに対して、「かならず」「1回だけ」m.endComputationを呼んでください。

もし、setIntervalやウェブソケットのイベントハンドラなど、繰り返し呼ばれるコールバックから再描画を行わせたい場合は、コールバックの外ではなく、コールバック関数の先頭でm.startComputationを呼び出してください。

setInterval(function() {
    m.startComputation(); //イベントハンドラの先頭で、他の処理よりも先に呼ぶ

    doStuff();

    m.endComputation(); //イベントハンドラの最後で、他の処理よりも後に呼ぶ
}

複数実行スレッドの統合

サードパーティ製のライブラリを統合する時に、MithrilのAPIの外で非同期メソッドが使っているものもあるでしょう。

自明でない非同期処理のコードとMithrilの自動再描画システムを統合するときには、確実にすべてのスレッドでm.startComputation / m.endComputationを呼び出す必要があります。

実行スレッドは基本的に、他の非同期スレッドが実行する前に、ある程度の量のコードを含んでいます。

複数の実行スレッドのコードと統合を行うには2つの方法があります。階層に分けて行う方法と、統括的に行う方法です。

階層に分けて統合

たくさんのさまざまなAPIがアプリケーションレベルで使われている時は、階層に分けて統合する方法がおすすめです。

次のサンプルは、多くのメソッドがサードパーティ製ライブラリを使っているコードを、階層に分けて統合したコードになります。メソッドの中には個別に使われるものもあれば、組み合わせて使われるものもあります。

doBothdoSomethingdoAnotherから呼び出されるため、m.startComputationを何度も呼んでいます。このコードは完璧に正しいコードです。jQuery.whenが呼ばれた後に、そこから3組のm.startComputation / m.endComputation呼び出しがあるため、3つの非同期描画が遅延されて、バッチ処理されます。

var doSomething = function(callback) {
    m.startComputation(); //`startComputation`は非同期のAJAXリクエスト前に呼ぶ

    return jQuery.ajax("/something").done(function() {
        if (callback) callback();

        m.endComputation(); //`endComputation`はコールバックの最後で呼び出す
    });
};
var doAnother = function(callback) {
    m.startComputation(); //`startComputation`は非同期のAJAXリクエスト前に呼ぶ

    return jQuery.ajax("/another").done(function() {
        if (callback) callback();
        m.endComputation(); //`endComputation`はコールバックの最後で呼び出す
    });
};
var doBoth = function(callback) {
    m.startComputation(); //非同期の同期メソッドの前に`startComputation`を呼び出す

    jQuery.when(doSomething(), doAnother()).then(function() {
        if (callback) callback();

        m.endComputation(); //`endComputation`はコールバックの最後で呼び出す
    })
};

統括的な統合

非同期の操作が、常に同じ手順で使われている場合には、統括的な統合がおすすめです。階層に分けた統合と比べると、m.startComputation / m.endComputation呼び出しが最小限になります。

次のサンプルには、サードパーティライブラリを使った複雑に入り組んだAJAXリクエストのかたまりがあります。

var doSomething = function(callback) {
    m.startComputation(); //すべての処理の前に`startComputation`を呼び出す

    jQuery.ajax("/something").done(function() {
        doStuff();
        jQuery.ajax("/another").done(function() {
            doMoreStuff();
            jQuery.ajax("/more").done(function() {
                if (callback) callback();

                m.endComputation(); //すべての処理の後に`endComputation`を呼び出す
            });
        });
    });
};