如何在Backbone.js中处理初始化和渲染子视图?

时间:2012-02-18 02:13:39

标签: javascript backbone.js

我有三种不同的方法来初始化和渲染视图及其子视图,并且每个视图都有不同的问题。我很想知道是否有更好的方法可以解决所有问题:


情景一:

在父级的初始化函数中初始化子级。这样,并不是所有内容都会陷入渲染状态,因此渲染时阻塞较少。

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结构,以便获得所需的顺序。这意味着更改模板可能需要更新视图的渲染功能。


情景二:

初始化父级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()现在变得必要了,这只是在第一个场景中的后续调用中需要的一点点消极。

情景三:

使用父render()方法初始化子项。

initialize : function () {

    //parent init stuff
},

render : function () {

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

    this.child = new Child();

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

问题:

  • 这意味着现在必须将渲染函数与所有初始化逻辑联系起来。

  • 如果我编辑其中一个子视图的状态,然后在父视图上调用render,将创建一个全新的子节点,并且它的所有当前状态都将丢失。对于内存泄漏而言,这似乎也有点冒险。


非常好奇让你的家伙接受这个。你会使用哪种场景?还是有第四个神奇的解决所有这些问题?

你有没有跟踪视图的渲染状态?说一个renderedBefore标志?看起来真的很笨拙。

7 个答案:

答案 0 :(得分:258)

这是一个很好的问题。 Backbone很棒,因为它缺乏假设,但它确实意味着你必须(决定如何)自己实现这样的事情。在查看了我自己的东西之后,我发现我(有点)使用了场景1和场景2的混合。我不认为第四个神奇的场景存在,因为,简单地说,你在场景1和场景中所做的一切。 2必须完成。

我认为最简单的解释我是如何用一个例子来处理它的。假设我将这个简单的页面分解为指定的视图:

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包含2个子视图,InfoViewPhoneListView以及一些额外的div,其中一个#name需要在某个时候设置。 PhoneListView拥有自己的子视图,一组PhoneView条目。

关于你的实际问题。我根据视图类型处理初始化和渲染。我将观点分为两类:Parent观看次数和Child次观看次数。

它们之间的区别很简单,Parent视图可以保存子视图,而Child视图则不会。因此,在我的示例中,ParentViewPhoneListViewParent次观看,而InfoViewPhoneView条目是Child次观看。

就像我之前提到的,这两个类别之间的最大区别在于它们是否允许渲染。在一个完美的世界中,我希望Parent个视图只渲染一次。当模型发生变化时,由子视图决定是否处理任何重新渲染。另一方面,Child次观看我可以随时重新呈现,因为他们没有任何其他依赖于他们的观点。

更详细一点,对于Parent次观看,我喜欢initialize个函数做一些事情:

  1. 初始化我自己的观点
  2. 渲染我自己的观点
  3. 创建并初始化所有子视图。
  4. 在我的视图中为每个子视图分配一个元素(例如InfoView将被分配#info)。
  5. 第1步非常自我解释。

    第2步,完成渲染,以便在我尝试分配子视图之前,子视图所依赖的任何元素都已存在。通过这样做,我知道所有的孩子events都将被正确设置,我可以根据需要重新渲染他们的块,而不必担心必须重新委派任何东西。我实际上并没有render这里有任何儿童观点,我允许他们在自己的initialization内进行观察。

    步骤3和4实际上是在创建子视图时传递el的同时处理的。我喜欢在这里传递一个元素,因为我觉得父母应该确定允许孩子放置其内容的位置。

    对于渲染,我尝试使Parent视图非常简单。我希望render函数除了呈现父视图之外什么都不做。没有事件委托,没有渲染子视图,没有。只是一个简单的渲染。

    有时这并不总是奏效。例如,在上面的示例中,只要模型中的名称发生更改,就需要更新#name元素。但是,此块是ParentView模板的一部分,不由专用的Child视图处理,因此我可以解决这个问题。我将创建某种subRender函数,替换#name元素的内容,而不必删除整个#parent元素。这可能看起来像一个黑客,但我真的发现它比担心重新渲染整个DOM和重新附加元素等更好。如果我真的想让它干净,我会创建一个新的Child视图(类似于InfoView)来处理#name块。

    现在,对于Child次观看,initialization非常类似于Parent次观看,只是没有创建任何进一步的Child观看次数。所以:

    1. 初始化我的观点
    2. 设置绑定侦听我关心的模型的任何更改
    3. 渲染我的观点
    4. 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类似,您只需在initializationrender函数中使用更多逻辑来处理集合。你如何处理这个集合真的取决于你,但你至少需要听取集合事件并决定你想要渲染的方式(追加/删除,或者只是重新渲染整个块)。我个人喜欢添加新视图并删除旧视图,而不是重新渲染整个视图。

      PhoneView几乎与InfoView相同,只是听取它关心的模型更改。

      希望这有点帮助,如果有什么令人困惑或不够详细,请告诉我。

答案 1 :(得分:6)

我不确定这是否直接回答了你的问题,但我认为这是相关的:

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

我设置这篇文章的背景当然是不同的,但我认为我提供的两个解决方案,以及每个解决方案的优缺点,都应该让你朝着正确的方向前进。

答案 2 :(得分:6)

对我来说,通过某种标志来区分视图的初始设置和后续设置似乎不是世界上最糟糕的想法。为了使这个干净简单,应该将标志添加到您自己的视图中,该视图应该扩展Backbone(Base)视图。

与Derick相同我不完全确定这是否直接回答了你的问题,但我认为在这方面可能至少值得一提。

  

Also see: Use of an Eventbus in Backbone

答案 3 :(得分:3)

Kevin Peel给出了一个很好的答案 - 这是我的tl; dr版本:

initialize : function () {

    //parent init stuff

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

    this.child = new Child();
},

答案 4 :(得分:2)

我试图避免这些视图之间的耦合。我通常有两种方式:

使用路由器

基本上,您让路由器功能初始化父视图和子视图。所以视图彼此不了解,但路由器处理它们。

将相同的el传递给两个视图

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

两者都知道相同的DOM,你可以随意订购它们。

答案 5 :(得分:2)

我所做的是给每个孩子一个身份(Backbone已经为你做了这个:cid)

当Container进行渲染时,使用'cid'和'tagName'为每个子节点生成占位符,因此在模板中,子节点不知道Container将放置它的位置。

<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();

不需要指定的占位符,Container只生成占位符而不是子窗体的DOM结构。 Cotainer和Children仍然只生成一次DOM元素。

答案 6 :(得分:1)

这是一个用于创建和渲染子视图的轻量级混合,我认为这解决了该线程中的所有问题:

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

此插件采用的方法是在呈现父视图的第一次时间之后创建和呈现子视图。然后,在后续渲染父视图时,$ .detach子视图元素,重新渲染父元素,然后将子视图元素插入适当的位置并重新渲染它们。这样,子视图对象可以在后续渲染中重复使用,而无需重新委派事件。

请注意,集合视图的情况(集合中的每个模型都用一个子视图表示)是完全不同的,我认为值得讨论/解决方案。我知道的最佳通用解决方案是CollectionView in Marionette

编辑:对于集合视图案例,您可能还需要查看this more UI focused implementation,如果您需要根据点击选择模型和/或拖放进行重新排序。