【问题标题】:How to handle initializing and rendering subviews in Backbone.js?如何在 Backbone.js 中处理初始化和渲染子视图?
【发布时间】:2012-03-09 10:10:36
【问题描述】:

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


场景一:

在父级的初始化函数中初始化子级。这样一来,并不是所有东西都卡在渲染中,从而减少了渲染阻塞。

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

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

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

问题:

  • 最大的问题是第二次在父节点上调用渲染会移除所有子节点的事件绑定。 (这是因为 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();
}

问题:

  • 这意味着渲染函数现在也必须与所有初始化逻辑绑定在一起。

  • 如果我编辑其中一个子视图的状态,然后在父视图上调用渲染,则会创建一个全新的子视图,并且其所有当前状态都将丢失。这似乎也可能导致内存泄漏。


真的很想得到你的家伙对此的看法。你会使用哪种场景?还是有第四种神奇的方法可以解决所有这些问题?

您是否曾经跟踪过视图的渲染状态?说renderedBefore 标志?看起来真的很笨拙。

【问题讨论】:

  • 我通常不会在父视图上对子视图的引用进行排序,因为大多数通信都是通过模型/集合以及由这些更改触发的事件发生的。虽然第三种情况最接近我大部分时间使用的情况。案例 1 有意义的地方。此外,大多数时候您不应该重新渲染整个视图,而是重新渲染已更改的部分
  • 当然,我肯定尽可能只渲染更改的部分,但即便如此,我认为渲染功能应该是可用的并且无论如何都是非破坏性的。您如何处理没有对父项中子项的引用?
  • 通常我会监听与子视图关联的模型上的事件——如果我想做一些更自定义的事情,那么我会将事件绑定到子视图。如果需要这种“通信”,那么我通常会创建辅助方法来创建绑定事件等的子视图。
  • 有关更高级别的相关讨论,请参阅:stackoverflow.com/questions/10077185/…。
  • 场景二,为什么一定要在setElement()之后调用delegateEvents()?根据文档:“......并将视图的委托事件从旧元素移动到新元素”,setElement 方法本身应该处理事件重新委托。

标签: javascript backbone.js


【解决方案1】:

这是一个很好的问题。 Backbone 很棒,因为它没有做任何假设,但这确实意味着您必须(决定如何)自己实现这样的事情。看完我自己的东西后,我发现我(有点)混合使用场景 1 和场景 2。我认为不存在第四个神奇场景,因为简单地说,你在场景 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 与图表的匹配方式非常明显。

ParentView 包含 2 个子视图,InfoView 和 PhoneListView 以及一些额外的 div,其中一个,#name,需要在某些时候设置。 PhoneListView 拥有自己的子视图,PhoneView 条目数组。

接着你的实际问题。我根据视图类型以不同方式处理初始化和渲染。我将我的视图分为两种类型,Parent 视图和 Child 视图。

它们之间的区别很简单,Parent 视图拥有子视图,而Child 视图则没有。所以在我的示例中,ParentView 和 PhoneListView 是 Parent 视图,而 InfoView 和 PhoneView 条目是 Child 视图。

就像我之前提到的,这两个类别之间的最大区别在于它们何时被允许渲染。在一个完美的世界中,我希望 Parent 视图只渲染一次。当模型更改时,由他们的子视图处理任何重新渲染。 Child 视图,另一方面,我允许在他们需要的任何时候重新渲染,因为他们没有任何其他依赖于它们的视图。

更详细一点,对于Parent 视图,我喜欢我的initialize 函数来做一些事情:

  1. 初始化我自己的视图
  2. 呈现我自己的视图
  3. 创建和初始化任何子视图。
  4. 在我的视图中为每个子视图分配一个元素(例如,InfoView 将被分配#info)。

第 1 步很容易解释。

第 2 步(渲染)已完成,以便子视图所依赖的任何元素在我尝试分配它们之前已经存在。通过这样做,我知道所有子 events 将被正确设置,并且我可以根据需要多次重新渲染他们的块,而不必担心必须重新委托任何东西。我实际上在render 没有任何子视图,我允许他们在自己的initialization 中这样做。

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

对于渲染,我尽量让Parent 视图保持简单。我希望render 函数除了渲染父视图之外什么也不做。没有事件委托,没有子视图的渲染,什么都没有。只是一个简单的渲染。

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

现在对于 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。如果模型发生变化,此块将重新渲染自身,而不会影响任何其他视图。

PhoneListView 与ParentView 类似,您只需要在initialization 和render 函数中增加一点逻辑来处理集合。如何处理集合完全取决于您,但您至少需要监听集合事件并决定如何呈现(追加/删除,或者只是重新呈现整个块)。我个人喜欢添加新视图并删除旧视图,而不是重新渲染整个视图。

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

希望这对您有所帮助,如果有任何令人困惑或不够详细的地方,请告诉我。

【讨论】:

  • 非常详尽的解释谢谢。不过问题是,我听说并倾向于同意在 initialize 方法中调用 render 是一种不好的做法,因为它会阻止您在不想立即渲染的情况下提高性能。您对此有何看法?
  • 当然。嗯......我只尝试初始化应该立即显示的视图(或可能在不久的将来显示,例如现在隐藏但在单击编辑链接时显示的编辑表单),因此它们应该立即呈现。如果说,我有一个视图需要一段时间才能渲染的情况,我个人不会在需要显示之前创建视图本身,因此在必要之前不会进行初始化和渲染。如果你有一个例子,我可以尝试更详细地了解我如何处理它......
  • 不能重新渲染 ParentView 是一个很大的限制。我有一种情况,我基本上有一个选项卡控件,其中每个选项卡显示不同的 ParentView。我想在第二次单击选项卡时重新渲染现有的 ParentView,但这样做会导致 ChildViews 中的事件丢失(我在 init 中创建子项,并在渲染中渲染它们)。我想我要么 1) 隐藏/显示而不是渲染,2) ChildViews 渲染中的 delegateEvents,或者 3) 在渲染中创建子级,或者 4) 不用担心性能。在出现问题之前,每次都重新创建 ParentView。嗯……
  • 我应该说这是我的限制。如果您不尝试重新渲染父级,此解决方案将正常工作:) 我没有一个好的答案,但我有与 OP 相同的问题。仍然希望找到正确的“主干”方式。在这一点上,我正在考虑隐藏/显示并且不重新渲染是我要走的路。
  • 如何将集合传递给子PhoneListView?
【解决方案2】:

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

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

当然,我撰写本文的背景有所不同,但我认为我提供的两种解决方案以及每种解决方案的优缺点应该能让你朝着正确的方向前进。

【讨论】:

    【解决方案3】:

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

    与 Derick 一样,我不完全确定这是否直接回答了您的问题,但我认为在这种情况下至少值得一提。

    Also see: Use of an Eventbus in Backbone

    【讨论】:

      【解决方案4】:

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

      initialize : function () {
      
          //parent init stuff
      
          this.render(); //ANSWER: RENDER THE PARENT BEFORE INITIALIZING THE CHILD!!
      
          this.child = new Child();
      },
      

      【讨论】:

        【解决方案5】:

        我正在尝试避免此类视图之间的耦合。我通常有两种方法:

        使用路由器

        基本上,您让路由器函数初始化父视图和子视图。所以视图彼此之间不知道,但路由器会处理这一切。

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

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

        两者都了解相同的 DOM,您可以随意订购它们。

        【讨论】:

          【解决方案6】:

          我所做的是给每个孩子一个身份(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 元素,而且只生成一次。

          【讨论】:

            【解决方案7】:

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

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

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

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

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

            【讨论】:

              猜你喜欢
              • 2013-10-21
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2013-05-19
              • 1970-01-01
              • 2013-06-30
              相关资源
              最近更新 更多