m.redraw



Redraws the view. コンポーネントのアクティベートには、m.mount()か、m.route()を使ってください。

m.redrawを呼ぶと、AJAXリクエストや他の非同期サービスが完了しているかどうかに関わらず、再描画処理を起動します。そのため、強制再描画時に、未初期化の変数アクセスが発生しないようにテンプレートでのnullチェックを確実に行うようにしてください。

もし非同期のモデルレベルのサービスを開発しているとします。コードが実行されていても、Mithrilがビューを更新しないことに気づきました。この場合は、m.startComputationm.endComputationを呼んで、Mithrilの自動再描画システムと統合します。

テンプレートでは適切にnullチェックが行われていると仮定すると、m.redrawはローディングのインジケータ表示したり、 offsetWidthscrollHeightの値を元にDOMの状態を変更しにいく時には便利です。


computationメソッドとm.redrawの違い

m.startComputation / m.endComputationのペアは「スタックされる」ように設計されています。例えば、複数の非同期サービスのそれぞれがこの呼出のペアを持っており、すべてのリクエストが完了するまで再描画を待たせることができます。これとは対照的にm.redrawは「アグレッシブ」です。このメソッドは、呼ばれた回数だけ再描画を行おうとします(実際には、同一フレーム内の呼び出しはまとめます)。m.redrawを使うと、AJAX呼び出しの完了前に再描画を実行させることができます。データの存在のチェックを行わないと、null参照例外が発生する恐れがあります。

そのため、複数の非同期アクセスが完了する前に再描画が即座に行われないようにするために、computationメソッドを使うほうが推奨です。

computationメソッドを使うと、未完了のデータをテンプレートが利用することがなくなります。しかし、テンプレート内でnullになりえる可能性がある場合は常にチェックコードを書くことが大切です。新しく導入したサードパーティライブラリがm.redrawを呼び出す可能性がありますし、何かしらの機能を実装する時に積極的な再描画を行う必要が出てくる可能性があるからです。

nullになる可能性がある場合の対処法としては、m.requestinitialValueオプションを指定するか、基本的なnullチェック(例: data ?m("div", data) : null)を行う方法があります。


再描画戦略の変更

Mithrilの再描画の実行パターンを変更する必要があれば、m.redraw.strategy getter-setterを、"all""diff""none"のどれかに設定してください。これを設定すると、次にスケジュールされている再描画の時点から、新しい方法が適用されます。Mithirlのデフォルト"all"で、コントローラのコンストラクタが実行される・イベントハンドラが起動される前に、差分を取得します。

再描画が完了すると、Mithrilは再描画がラウトの変更に起因するものかどうかによって、この設定を"all"か"diff"に変更します。

上記の説明に書かれているように、再描画のコンテキストに入る時にこのフラグはリセットされるため、再描画コンテキスト外でこのフラグを設定しても無視されます。

このフラグが"all"に設定されると、Mithrilは現在のビューをすべて破棄し、ゼロから再描画します。この動作はラウト間の遷移時のデフォルトの動作です。

このフラグが"diff"に設定されると、Mithrilは古いビューと新しいビューの間の差分の検知を行い必要な箇所にだけ差分を適用します。

フラグが"none"に設定されるとMithrilは次の再描画をスキップします。繰り返しになりますが、このフラグを設定する必要はありません。Mithrilが自動で行います。

var Component1 = {
    controller: function() {
        //このコンポーネントはラウトの変更時にもゼロからビューを作りなおすのではなく、差分検知を行う
        //これにより、エレメントの再生成が不要な時は、configコンテキストがラウト変更時にも破棄されない
        m.redraw.strategy("diff")
    },
    view: function() {
        return m("h1", {config: Component1.config}, "test") //すべてのラウトが同じコンテンツを表示する
    },
    config: function(el, isInit, ctx) {
        if (!isInit) ctx.data = "foo" //この初期化はラウトの変更に関わらず一度だけ実行したい
    }
}

再描画戦略を変更する一般的な理由としては次のようなものがあります:

  • keypressイベントで受け取ったキーコードが処理対象外だったので再描画を避けたい

    //モデル
    var saved = false
    function save(e) {
       if (e.keyCode == 13) {
           //デフォルトでイベントハンドラは自動再描画を起動するため再描画が行われる
           saved = true
       }
       else {
           //他のキーについては無視したいので、再描画も行わない
           m.redraw.strategy("none")
       }
    }
    
    //ビュー
    var view = function() {
       return m("div", [
           m("button[type=button]", {onkeypress: save}, "保存"),
           saved ?"Saved" : ""
       ])
    }
    
  • ラウト変更時に全画面の再構築を避けて、サードパーティのコンポーネントのパフォーマンス劣化を避ける。

    //ゼロから再描画するのではなく、ラウト変更時にも差分検知をする
    //ラウト変更後も`<input>`エレメントと、それに追加されたサードパーティプラグインが維持される
    var Component1 = {
       controller: function() {
           m.redraw.strategy("diff")
       },
       view: function() {
           return m("div", [
               m("h1", "Hello Foo"),
               m("input", {config: plugin}) //`plugin`がサードパーティのライブラリを初期化するものとする
           ])
       }
    }
    
    var Component2 = {
       controller: function() {
           m.redraw.strategy("diff")
       },
       view: function() {
           return m("div", [
               m("h1", "Hello Bar"),
               m("input", {config: plugin}) //`plugin`がサードパーティのライブラリを初期化するものとする
           ])
       }
    }
    
    m.route(document.body, "/foo", {
       "/foo": Component1,
       "/bar": Component2,
    })
    

再描画の戦略は、ページ内の全コンポーネントのテンプレートツリーに影響を与えるグローバルな設定であることに注意してください。アプリケーション内の一部分の再描画だけを止めたい場合は、サブツリーディレクティブを参照してください。

ctx.retainフラグを使うと、個別のエレメントに対してラウトの変更であっても、常に差分検知を行わずに再生成させないように指示することもできます。ラウト間でDOMの状態を維持する必要があるときは、m.redraw.strategy("diff")ではなく、ctx.retainフラグを使うのが推奨です。


イベントによる再描画の阻止

イベント内の特定の条件について考慮した時に、その条件に合わなければ再描画を行いたくない、と思うこともよくあるでしょう。例えば、ユーザがスペースバーを推した時だけ再描画を行い、そうでない場合は再描画はムダなのでやりたいくない、ということもあります。この場合、m.redraw.strategy("none")を呼ぶことで、再描画をスキップできます。

m("input", {onkeydown: function(e) {
    if (e.keyCode == 13) vm.save() //`enter`キーが押された時だけ再描画をする
    else m.redraw.strategy("none") //そうでなければ無視する
}})

注意すべきこととしては、m.redraw.strategy("none") で戦略を"none"に設定しても、そのフレームで同期的に行われる再描画にしか効果がありません。イベントハンドラが完了すると、再び"diff"に戻ります。

このメソッドを使って"none"に設定したにもかかわらず、start/endComputationm.redrawm.requestによって非同期での再描画のトリガーが引かれた場合は、 "diff" 戦略で再描画が先のフレームで行われます。

また、m.redraw.strategy("none")を呼んだ後にm.redrawを同期的に呼んだ場合には、戦略は"diff"にリセットされます。

最後に、ユーザアクションがoninputとonkeypressなどの複数のイベントハンドラを起動したり、複数の祖先エレメントのイベントハンドラに対してバブリングした場合は、デフォルトではすべてのイベントが再描画を起動します。この場合、1つのハンドラでnoneに戦略を変更しても、strategy("none") は非同期の再描画に効果を与えないため、他のハンドラの再描画戦略に影響を与えることはありません。


強制再描画

もしブラウザの通常の再描画サイクルよりも前に再描画を行わせたい場合は、m.redrawのオプションとしてtrueを渡すと、強制的に同期再描画を行わせることができます。

m.redraw(true) // 強制

通常は、offsetTopやCSSルールなど、ブラウザの再描画を必要とする、DOMの同期的な値読み込みが必要になった時だけにすべきです。もしDOMの値を読む必要が出てきた場合は、すべての必要なDOMの値を一度に読むようにしてください。少しずつ読み書きすると、高価なブラウザの再描画が何度も発生します。


シグニチャ

シグニチャの読み方

void redraw([Boolean forceSync]) { GetterSetter strategy }

where:
    GetterSetter :: String getterSetter([String value])
  • Boolean forceSync (optional)

    もしこれがtrueに設定されると、同期的に再描画を行う。デフォルトでは、イベントハンドラは非同期で再描画を行うようにスケジュールします。これにより、同じ入力フォームに対して同時に使われるkeypressとinputなどの複数のイベントを再描画前にまとめて実行し、再描画の回数を減らすことができます。デフォルト値はfalseです。

  • m.redraw.strategy

    GetterSetter strategy

    m.redraw.strategy getter-setterは次のコンポーネントの再描画をどのように行うかを設定する。これには次の3つの値のうちの1つを設定します:

    • "all" - すべて破棄してDOMツリーを再構築する
    • "diff" - 必要があればDOMエレメントを更新する
    • "none" - DOMツリーを変更せずにそのままにする

    この値は、コントローラとイベントハンドラの中で設定することができます。これは次に行われるの再描画の戦略にのみ作用します。コントローラの構築が行われると、Mithrilは内部的にこのフラグを"all"に設定し、再描画が行われると"diff"に設定します。

    引数を渡さずにこの関数を呼ぶと、現在の再描画戦略を返します。