web-dev-qa-db-ja.com

Backbone.jsでサブビューの初期化とレンダリングを処理する方法は?

ビューとそのサブビューを初期化してレンダリングするには3つの異なる方法があり、それぞれに異なる問題があります。すべての問題を解決するより良い方法があるかどうか知りたいです:


シナリオ1:

親の初期化関数で子を初期化します。この方法では、すべてがレンダリングでスタックするわけではないため、レンダリングのブロックが少なくなります。

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

    this.$el.html(this.template());

    this.child.render().appendTo(this.$('.container-placeholder');
}

問題点:

  • 最大の問題は、親で2回renderを呼び出すと、すべての子イベントバインディングが削除されることです。 (これはjQueryの$.html()がどのように機能するかによるものです。)代わりにthis.child.delegateEvents().render().appendTo(this.$el);を呼び出すことで軽減できますが、最初の、そしてほとんどの場合、不必要に多くの作業をしています。

  • 子を追加することにより、render関数に親のDOM構造の知識を持たせ、必要な順序を取得できます。つまり、テンプレートを変更するには、ビューのレンダリング関数を更新する必要がある場合があります。


シナリオ2:

まだ親のinitialize()の子を初期化しますが、追加する代わりに、setElement().delegateEvents()を使用して、子を親テンプレートの要素に設定します。

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

    this.$el.html(this.template());

    this.child.setElement(this.$('.placeholder-element')).delegateEvents().render();
}

問題点:

  • これにより、delegateEvents()が必要になります。これは、最初のシナリオの後続の呼び出しでのみ必要であるという点でわずかにマイナスです。

シナリオ3:

代わりに、親のrender()メソッドで子を初期化します。

initialize : function () {

    //parent init stuff
},

render : function () {

    this.$el.html(this.template());

    this.child = new Child();

    this.child.appendTo($.('.container-placeholder').render();
}

問題点:

  • これは、レンダリング関数をすべての初期化ロジックと同様に結び付ける必要があることを意味します。

  • 子ビューの1つの状態を編集してから親でrenderを呼び出すと、完全に新しい子が作成され、現在の状態がすべて失われます。これは、メモリリークの危険性もあるようです。


あなたの仲間にこれを理解してもらうことに本当に興味があります。どのシナリオを使用しますか?または、これらすべての問題を解決する4つ目の魔法のようなものはありますか?

ビューのレンダリング状態を追跡したことがありますか? renderedBeforeフラグを言う?本当にジャンキーそうです。

198

これは素晴らしい質問です。 Backboneは前提条件がないため優れていますが、このようなものを自分で実装する(方法を決定する)必要があるということです。自分のものを調べた後、私はシナリオ1とシナリオ2を組み合わせて使用​​していることに気付きました。シナリオ1と2で行うすべてのことを十分に行う必要があるため、4番目の魔法のシ​​ナリオは存在しません。できた。

私がそれをどのように扱いたいかを例で説明するのが最も簡単だと思います。この単純なページを指定されたビューに分割したとします。

Page Breakdown

HTMLがレンダリングされた後、次のようになっているとします。

<div id="parent">
    <div id="name">Person: Kevin Peel</div>
    <div id="info">
        First name: <span class="first_name">Kevin</span><br />
        Last name: <span class="last_name">Peel</span><br />
    </div>
    <div>Phone Numbers:</div>
    <div id="phone_numbers">
        <div>#1: 123-456-7890</div>
        <div>#2: 456-789-0123</div>
    </div>
</div>

うまくいけば、HTMLが図とどのように一致するかはかなり明白です。

ParentViewは、InfoViewPhoneListViewの2つの子ビューと、いくつかの余分なdivを保持します。そのうちの1つは#nameを設定する必要があります。 PhoneListViewは、独自の子ビュー、PhoneViewエントリの配列を保持します。

あなたの実際の質問に続きます。ビュータイプに基づいて、初期化とレンダリングを別々に処理します。ビューをParentビューとChildビューの2つのタイプに分けます。

それらの違いは単純です。Parentビューは子ビューを保持しますが、Childビューは保持しません。したがって、私の例では、ParentViewおよびPhoneListViewビューはParentビューであり、InfoViewおよびPhoneViewエントリーはChildビューです。

前述したように、これら2つのカテゴリの最大の違いは、レンダリングが許可される場合です。完璧な世界では、Parentビューが一度だけレンダリングされるようにします。モデルが変更されたときに再レンダリングを処理するのは、子ビュー次第です。一方、Childビューは、それらに依存する他のビューがないため、必要なときにいつでも再レンダリングできます。

もう少し詳しく説明すると、Parentビューについては、initialize関数がいくつかのことをするのが好きです:

  1. 自分のビューを初期化する
  2. 独自のビューをレンダリングする
  3. 子ビューを作成して初期化します。
  4. ビュー内の各子ビューに要素を割り当てます(たとえば、InfoViewには#infoが割り当てられます)。

ステップ1は自明です。

ステップ2のレンダリングは、子ビューが依存する要素が割り当てられる前に既に存在するように行われます。これを行うことで、すべての子eventsが正しく設定されることがわかり、再デリゲートすることを心配せずに、何度でもブロックを再レンダリングできます。ここでは実際にrenderの子ビューはありません。自分のinitialization内で子ビューを許可します。

ステップ3と4は、子ビューの作成中にelを渡すと同時に実際に処理されます。ここで要素を渡すのが好きです。親が自分のビューのどこに子がコンテンツを配置できるかを判断する必要があると思うからです。

レンダリングについては、Parentビューでかなりシンプルにしようとしています。 render関数は、親ビューをレンダリングする以外のことは何もしません。イベントの委任、子ビューのレンダリング、なし。単純なレンダリングです。

ただし、これが常に機能するとは限りません。たとえば、上記の私の例では、モデル内の名前が変更されるたびに#name要素を更新する必要があります。ただし、このブロックはParentViewテンプレートの一部であり、専用のChildビューで処理されないため、回避します。 only#name要素のコンテンツを置き換え、#parent要素全体を破棄する必要のない、ある種のsubRender関数を作成します。これはハックのように思えるかもしれませんが、DOM全体を再レンダリングし、要素などを再アタッチすることを心配するよりもうまく機能することが本当にわかりました。本当にきれいにしたい場合は、#nameブロックを処理する新しいChildビュー(InfoViewに類似)を作成します。

現在、Childビューの場合、initializationビューはParentビューと非常によく似ていますが、追加のChildビューは作成されません。そう:

  1. ビューを初期化する
  2. セットアップは、関心のあるモデルへの変更をリッスンします
  3. ビューをレンダリングする

Childビューのレンダリングも非常にシンプルで、elのコンテンツをレンダリングして設定するだけです。繰り返しますが、委任やそのようなものをいじることはありません。

ParentViewがどのように見えるかを示すサンプルコードを次に示します。

var ParentView = Backbone.View.extend({
    el: "#parent",
    initialize: function() {
        // Step 1, (init) I want to know anytime the name changes
        this.model.bind("change:first_name", this.subRender, this);
        this.model.bind("change:last_name", this.subRender, this);

        // Step 2, render my own view
        this.render();

        // Step 3/4, create the children and assign elements
        this.infoView = new InfoView({el: "#info", model: this.model});
        this.phoneListView = new PhoneListView({el: "#phone_numbers", model: this.model});
    },
    render: function() {
        // Render my template
        this.$el.html(this.template());

        // Render the name
        this.subRender();
    },
    subRender: function() {
        // Set our name block and only our name block
        $("#name").html("Person: " + this.model.first_name + " " + this.model.last_name);
    }
});

subRenderの実装をここで見ることができます。変更をsubRenderではなくrenderにバインドすることにより、ブロック全体をブラストして再構築することを心配する必要がなくなります。

InfoViewブロックのサンプルコードは次のとおりです。

var InfoView = Backbone.View.extend({
    initialize: function() {
        // I want to re-render on changes
        this.model.bind("change", this.render, this);

        // Render
        this.render();
    },
    render: function() {
        // Just render my template
        this.$el.html(this.template());
    }
});

ここでは、バインドが重要な部分です。自分のモデルにバインドすることで、自分でrenderを手動で呼び出すことを心配する必要がなくなりました。モデルが変更されると、このブロックは他のビューに影響を与えることなくそれ自体を再レンダリングします。

PhoneListViewParentViewに似ています。コレクションを処理するには、initialization関数とrender関数の両方にもう少しロジックが必要です。コレクションの処理方法は本当にあなた次第ですが、少なくともコレクションイベントをリッスンし、レンダリング方法(ブロック全体の追加/削除、または再レンダリング)を決定する必要があります。個人的には、ビュー全体を再レンダリングするのではなく、新しいビューを追加して古いビューを削除するのが好きです。

PhoneViewは、InfoViewとほぼ同じで、関心のあるモデルの変更のみをリッスンします。

うまくいけば、これが少し助けになったと思います。何かが混乱したり、詳細が十分でない場合はお知らせください。

258
Kevin Peel

私にとっては、何らかのフラグを介してビューの初期設定とその後の設定を区別することは、世界で最悪の考えとは思えません。これをクリーンで簡単にするために、バックボーン(ベース)ビューを拡張する独自のビューにフラグを追加する必要があります。

デリックと同じように、これがあなたの質問に直接答えるかどうかは完全にはわかりませんが、少なくともこの文脈で言及する価値があると思います。

参照:バックボーンでのイベントバスの使用

6
Bruiser

これがあなたの質問に直接答えるかどうかはわかりませんが、関連があると思います:

http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/

もちろん、この記事を設定するコンテキストは異なりますが、私が提供する2つのソリューションとそれぞれの長所と短所は、正しい方向に進むはずです。

6
Derick Bailey

ケビン・ピールは素晴らしい答えを与えます-私のtl; drバージョンは次のとおりです。

initialize : function () {

    //parent init stuff

    this.render(); //ANSWER: RENDER THE PARENT BEFORE INITIALIZING THE CHILD!!

    this.child = new Child();
},
3
Nate Barr

私がやることは、各子供にアイデンティティを与えることです(Backboneはすでにあなたのためにそれを行っています:cid)

コンテナがレンダリングを実行すると、「cid」と「tagName」を使用してすべての子のプレースホルダーが生成されるため、テンプレートでは、子はコンテナによってどこに配置されるかわかりません。

<tagName id='cid'></tagName>

あなたが使用できるより

Container.render()
Child.render();
this.$('#'+cid).replaceWith(child.$el);
// the rapalceWith in jquery will detach the element 
// from the dom first, so we need re-delegateEvents here
child.delegateEvents();

指定されたプレースホルダーは不要で、コンテナは子のDOM構造ではなくプレースホルダーのみを生成します。 CotainerとChildrenはまだ1回だけ独自のDOM要素を生成しています。

2
Villa

私はこのようなビュー間の結合を避けようとしています。私が通常行う方法は2つあります。

ルーターを使用する

基本的に、ルーター関数で親ビューと子ビューを初期化できます。そのため、ビューには互いの知識はありませんが、ルーターがすべてを処理します。

同じエルを両方のビューに渡す

this.parent = new Parent({el: $('.container-placeholder')});
this.child = new Child({el: $('.container-placeholder')});

どちらも同じDOMの知識があり、好きなように注文できます。

サブビューを作成およびレンダリングするための軽量ミックスインを次に示します。これは、このスレッドのすべての問題に対処すると思います。

https://github.com/rotundasoftware/backbone.subviews

このプラグインがとるアプローチは、親ビューがレンダリングされるfirst時間の後にサブビューを作成およびレンダリングすることです。次に、親ビューの後続のレンダリングで、サブビュー要素を$ .detachし、親を再レンダリングしてから、適切な場所にサブビュー要素を挿入し、それらを再レンダリングします。このように、サブビューオブジェクトは後続のレンダリングで再利用され、イベントを再デリゲートする必要はありません。

コレクションビューの場合(コレクション内の各モデルが1つのサブビューで表される場合)はまったく異なり、独自の議論/解決策に値することに注意してください。私がその場合に知っている最も一般的な解決策は、 MarionetteのCollectionView です。

編集:コレクションビューの場合、クリックおよび/またはドラッグアンドドロップに基づいて並べ替えのためにモデルを選択する必要がある場合は、 このUIに重点を置いた実装 も確認してください。

1
Brave Dave