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


199

ビューとそのサブビューを初期化してレンダリングする方法は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');
}

問題点:

  • 最大の問題は、もう一度親でrenderを呼び出すと、すべての子イベントバインディングが削除されることです。(これはjQueryがどのように機能するかが原因です$.html()。)これはthis.child.delegateEvents().render().appendTo(this.$el);代わりにを呼び出すことで軽減できますが、最初の最も頻繁なケースでは、不必要により多くの作業を行っています。

  • 子を追加することにより、レンダリング機能に親の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();
}

問題:

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

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


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

ビューのレンダリングされた状態を追跡したことがありますか?renderedBefore旗を言う?本当にぎこちないようです。


1
私は通常、親ビューの子ビューへの参照をソートしないので、ほとんどの通信は、モデル/コレクションおよびこれらへの変更によってトリガーされるイベントを通じて発生します。3番目のケースは、私がほとんどの場合使用しているものに最も近いですが。それが理にかなっているケース1。また、ほとんどの時間は、あなたが全体のビューではなく、変更した部分再レンダリングすべきではない
トム・火

確かに、可能な場合は必ず変更された部分だけをレンダリングしますが、それでも、レンダリング機能は使用可能であり、非破壊的である必要があります。親の子への参照がないことをどのように処理しますか?
Ian Storm Taylor

通常、子ビューに関連付けられているモデルのイベントをリッスンします。さらにカスタムを行いたい場合は、イベントをサブビューにバインドします。「通信」のこの種を必要とするならば、私は通常、などのイベントを結合するサブビューを作成するためのヘルパーメソッドを作成しています
トム・火

1
より上位の関連するディスカッションについては、stackoverflow.com / questions / 10077185 / …を参照してください。
ベンロバーツ

2
シナリオ2では、なぜ?のdelegateEvents()後に呼び出す必要があるのsetElement()ですか?ドキュメントに従って:「...そして、ビューの委任されたイベントを古い要素から新しい要素に移動します」、setElementメソッド自体がイベントの再委任を処理する必要があります。
rdamborsky 2013年

回答:


260

これは素晴らしい質問です。バックボーンは想定が不足しているため素晴らしいですが、このようなことを自分で実装する(方法を決定する)必要があることを意味します。自分のものを調べたところ、シナリオ1とシナリオ2が混在していることがわかりました。4番目の魔法のシ​​ナリオは、シナリオ1と2で行うことはすべて、完了しました。

例を挙げて、自分の扱い方を説明するのが最も簡単だと思います。この単純なページを指定されたビューに分割するとします。

ページの内訳

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が図とどのように一致するかはかなり明白です。

ParentView2つのビューを保持している、InfoViewPhoneListViewだけでなく、いくつかの余分なdiv要素の一つ、#nameいくつかの点に設定する必要があります。 エントリのPhoneListView配列である独自の子ビューを保持しPhoneViewます。

では、実際の質問に移ります。ビューのタイプに基づいて、初期化とレンダリングを異なる方法で処理します。私は自分の見解を2つのタイプ、ParentビューとChildビューに分けます。

それらの違いは単純です。Parentビューは子ビューを保持しますが、ビューは保持Childしません。したがって、私の例ではParentViewPhoneListViewParentビューですが、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ビューでは処理されないため、回避します。要素のコンテンツを置き換えるだけで、要素全体を破棄する必要のない、ある種のsubRender関数を作成します。これはハックのように思えるかもしれませんが、DOM全体の再レンダリングや要素の再接続などを心配する必要があるよりも、うまく機能することがわかりました。本当にクリーンにしたい場合は、ブロックを処理する(と同様の)新しいビューを作成します。#name#parentChildInfoView#name

Childビューの場合、これはビューinitializationとかなり似ていParentますが、それ以上Childビューを作成する必要はありません。そう:

  1. ビューを初期化する
  2. セットアップは、気になるモデルへの変更のリスニングをバインドします
  3. ビューをレンダリングする

Childビューのレンダリングも非常に単純で、レンダリングしてmyのコンテンツを設定するだけ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自分を呼び出すことを心配する必要がなくなります。モデルが変更されると、このブロックは他のビューに影響を与えることなく、自身を再レンダリングします。

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

PhoneViewほぼ同じになるInfoViewだけで、それが気にモデルチェンジを聞いて、。

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


8
本当に徹底的な説明ありがとうございます。質問ですが、すぐにレンダリングしたくない場合にパフォーマンスを向上させることができないためrenderinitializeメソッド内での呼び出しは悪い習慣だと聞いて同意する傾向があります。これについてどう思う?
Ian Storm Taylor

承知しました。うーん...私は今すぐに表示する必要があるビューを初期化しようとするだけです(または、近い将来表示される可能性があります。たとえば、現在非表示になっているが編集リンクをクリックすると表示される編集フォーム)。すぐにレンダリングする必要があります。ビューのレンダリングに時間がかかる場合がありますが、個人的には、表示するまでビュー自体を作成しないので、必要になるまで初期化とレンダリングは行われません。例があれば、私がそれをどのように処理するかについてさらに詳しく説明することができます...
Kevin Peel

5
ParentViewを再レンダリングできないことは大きな制限です。基本的にタブコントロールがあり、各タブに異なるParentViewが表示される状況があります。タブが2回クリックされたときに既存のParentViewを再レンダリングしたいのですが、そうすると、ChildViewのイベントが失われます(initで子を作成し、それらをレンダリングでレンダリングします)。私は、1)レンダーの代わりに非表示/表示、2)ChildViewsのレンダーでdelegateEvents、3)レンダーで子を作成、または4)パフォーマンスについて心配しないと思います。問題になる前に、毎回ParentViewを再作成してください。
うーん

私の場合それは制限であると言っておくべきです。このソリューションは、親を再レンダリングしない場合は問題なく機能します:)良い答えはありませんが、OPと同じ質問があります。適切な「バックボーン」の方法を見つけることをまだ望んでいます。この時点で、私は非表示/表示し、再レンダリングしないことが私のために行く方法だと考えています。
Paul Hoenecke

1
コレクションをどのように子に渡しPhoneListViewますか?
Tri Nguyen


6

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

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

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


3

ケビン・ピールは素晴らしい答えを出します-これが私のtl; drバージョンです:

initialize : function () {

    //parent init stuff

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

    this.child = new Child();
},

2

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

ルーターを使用する

基本的に、ルーター関数に親ビューと子ビューを初期化させます。したがって、ビューはお互いを認識していませんが、ルーターがすべてを処理します。

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

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

どちらも同じDOMの知識があり、必要に応じて注文できます。


2

私がすることは、各子供にアイデンティティを与えることです(バックボーンはあなたのためにすでにそれを行っています: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は、独自のDOM要素をまだ一度だけ生成しています。


1

これは、サブスレッドを作成およびレンダリングするための軽量なMixinです。これは、このスレッドのすべての問題に対処すると思います。

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

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.