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


199

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


方案一:

在父级的初始化函数中初始化子级。这样,并非所有内容都卡在渲染中,从而减少了渲染的阻塞。

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

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

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

问题:

  • 最大的问题是第二次在父级上调用render将删除所有childs事件绑定。(这是因为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,则将创建一个全新的子级,并且其所有当前状态都将丢失。这似乎也可以解决内存泄漏问题。


真的很想让你们接受这一点。您将使用哪种方案?还是有第四个神奇的魔法可以解决所有这些问题?

您是否曾经跟踪过View的渲染状态?说一个renderedBefore标志?看起来真的很简陋。


1
我通常不会在父视图上严格引用对子视图的引用,因为大多数通信是通过更改模型/集合和事件触发的。尽管第3种情况最接近我在大多数情况下使用的情况。情况1有意义。同样,在大多数情况下,您不应该重新渲染整个视图,而应该重新渲染已更改的部分
Tom Tu

当然,在可能的情况下,我绝对只渲染更改的部分,但即使如此,我仍然认为渲染功能应该可用并且无论如何都应该是无损的。您如何处理在父母中没有提及孩子的问题?
伊恩·风暴泰勒

通常,我在与子视图关联的模型上监听事件-如果我想做更多自定义操作,则将事件绑定到子视图。如果这样的“通信”是必需的,然后我通常在创建的helper方法,用于创建子视图结合的事件等
汤姆涂

1
有关更高级别的相关讨论,请参见:stackoverflow.com/questions/10077185/…
本·罗伯茨

2
在方案二中,为什么必须delegateEvents()setElement()?之后调用?按照文档:“ ...并将视图的委托事件从旧元素移动到新元素”,该setElement方法本身应处理事件的重新委托。
rdamborsky

Answers:


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如何与图表匹配。

ParentView拥有2个视图,InfoView并且PhoneListView还有一些额外的div,其中之一,#name,需要在某些点进行设置。 PhoneListView拥有自己的子视图(一组PhoneView条目)。

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

它们之间的区别很简单,Parent视图持有子视图,而Child视图不持有子视图。因此,在我的示例中,ParentViewPhoneListViewParent视图,而InfoViewPhoneView条目是Child视图。

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

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

  1. 初始化我自己的观点
  2. 呈现我自己的观点
  3. 创建并初始化所有子视图。
  4. 在我的视图中为每个子视图分配一个元素(例如InfoView将分配#info)。

第1步很容易解释。

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

实际上el,在创建子视图时,第3步和第4步实际上是在我传入的同时进行的。我喜欢在此处传递一个元素,因为我认为父母应该确定允许孩子放置其内容的位置。

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

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

现在,对于Child视图,该视图initializationParent视图非常相似,只是没有创建任何其他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方法内部进行调用是一种不好的做法,因为在您不想立即进行渲染的情况下,这样做会阻止您提高性能。你怎么看待这件事?
伊恩·斯托姆·泰勒

当然。嗯...我只尝试初始化应该立即显示的视图(或者可以在不久的将来显示,例如,现在隐藏但在单击编辑链接时显示的编辑表单),因此应立即进行渲染。如果说,我有一个视图需要一段时间才能渲染的情况,我个人不会在需要显示视图之前创建视图本身,因此除非必要,否则不会进行初始化和渲染。如果您有示例,我可以尝试进一步详细说明如何处理...
Kevin Peel 2012年

5
无法重新渲染ParentView是一个很大的限制。我遇到的情况是,我基本上有一个选项卡控件,其中每个选项卡都显示一个不同的ParentView。我想仅在第二次单击选项卡时重新渲染现有的ParentView,但是这样做会导致ChildViews中的事件丢失(我在init中创建子代,然后在render中对其进行渲染)。我猜我要么1)隐藏/显示而不是渲染,2)在ChildViews的渲染中委托事件,或者3)在渲染中创建子对象,或者4)不用担心性能问题。在出现问题之前,请每次重新创建ParentView。

我应该说这对我来说是一个限制。如果您不尝试重新渲染父级,则此解决方案会很好用:)我没有很好的答案,但我有与OP相同的问题。仍然希望找到正确的“骨干”方式。在这一点上,我正在考虑隐藏/显示并且不重新渲染是适合我的方法。
Paul Hoenecke 2012年

1
您如何将收藏品传递给孩子PhoneListView
Tri Nguyen


6

在我看来,通过某种标记来区分视图的初始设置和后续设置似乎不是世界上最糟糕的想法。为了使此操作变得简单明了,应将标志添加到您自己的视图中,该视图应扩展主干(基本)视图。

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

另请参阅:在主干中使用Eventbus


3

凯文·皮尔(Kevin Peel)提供了一个很好的答案-这是我的tl; dr版本:

initialize : function () {

    //parent init stuff

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

    this.child = new Child();
},

2

我试图避免像这样的观点之间的耦合。我通常有两种方法:

使用路由器

基本上,让路由器功能初始化父视图和子视图。因此,视图彼此之间并不了解,但是路由器会处理所有这些。

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

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

两者都具有相同的DOM知识,您可以随时订购它们。


2

我要做的是给每个孩子一个身份(Backbone已经为您完成了此操作:cid)

当Container进行渲染时,使用'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();

不需要指定的占位符,并且Container仅生成占位符,而不生成子代的DOM结构。Cotainer和Children仍仅生成一次自己的DOM元素。


1

这是用于创建和渲染子视图的轻巧混合器,我认为可以解决此线程中的所有问题:

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

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

请注意,集合视图(集合中的每个模型都由一个子视图表示)的情况是完全不同的,我认为应该有自己的讨论/解决方案。对于这种情况,我知道的最佳常规解决方案是Marionette中CollectionView

编辑:对于集合视图的情况,如果您需要基于单击和/或拖放以进行重新排序的模型选择,则您可能还想查看这个更加注重UI的实现

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.