如何在Backbone.js中处理子视图的初始化和呈现?

199

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


场景一:

在父视图的initialize函数中初始化子视图。这样,不是所有东西都被堵塞在render中,所以渲染时会减少阻塞。

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 标志?这似乎非常不稳定。


1
通常情况下,我不会在父视图上存储对子视图的引用,因为大部分通信是通过模型/集合以及由这些变化触发的事件进行的。尽管第三种情况最接近我通常使用的方法。只有在有意义的情况下才使用第一种情况。此外,在大多数情况下,你不应该重新渲染整个视图,而是应该重新渲染已更改的部分。 - Tom Tu
当可能时,我肯定只会渲染更改的部分,但即使如此,我认为渲染函数应该始终可用且非破坏性。如果父组件没有对子组件的引用,你会如何处理? - Ian Storm Taylor
通常我会监听与子视图相关联的模型上的事件 - 如果我想要做更多自定义的事情,那么我会将事件绑定到子视图上。如果需要这种“通信”,那么我通常会创建一个帮助方法来创建子视图,该方法会绑定事件等。 - Tom Tu
1
有关更高级别的讨论,请参见:https://dev59.com/FGkw5IYBdhLWcg3wMHkY#10941976。 - B Robster
2
在第二种情况下,为什么需要在setElement()之后调用delegateEvents()?根据文档:“...并将视图的委托事件从旧元素移动到新元素”,setElement方法本身应该处理事件重新委派。 - rdamborsky
7个回答

260
这是一个很好的问题。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)。

步骤1非常简单明了。

第二步,渲染是为了确保任何子视图依赖的元素在我尝试分配它们之前已经存在。通过这样做,我知道所有子事件将被正确设置,并且我可以重新渲染它们的块,而不必担心重新委派任何内容。我实际上没有在这里渲染任何子视图,我允许它们在其自己的初始化中执行。
第三步和第四步实际上是在创建子视图时同时处理的,因为我在创建子视图时传递了el。我喜欢在这里传递一个元素,因为我觉得父级应该确定子级在其自己的视图中可以放置其内容的位置。
对于渲染,我尽量让Parent视图保持简单。我希望render函数仅仅渲染父视图。没有委派事件,没有渲染子视图,什么也没有。只有一个简单的渲染。
有时候这并不总是有效的。例如在上面的例子中,当模型中的名称更改时,#name 元素需要更新。然而,这个区块是 ParentView 模板的一部分,而不是由专门的 Child 视图处理,因此我会绕过它。我将创建某种类型的 subRender 函数,仅替换 #name 元素的内容,而无需删除整个 #parent 元素。这可能看起来像是一个 hack,但我真的发现它比担心重新渲染整个 DOM 并重新连接元素之类的事情要好。如果我真的想让它变得干净,我会创建一个新的 Child 视图(类似于 InfoView),来处理 #name 区块。
现在对于 Child 视图,initializationParent 视图非常相似,只是不需要创建任何进一步的 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类似,只需在initializationrender函数中增加一些逻辑以处理集合。如何处理集合真的取决于您,但至少需要监听集合事件并决定如何呈现(追加/删除或仅重新呈现整个块)。我个人喜欢追加新视图并删除旧视图,而不是重新呈现整个视图。 PhoneViewInfoView几乎完全相同,只需要关注它关心的模型更改。
希望这有所帮助,请让我知道是否有任何困惑或不够详细。

8
非常详细的解释,谢谢。不过我有一个问题,我听说并且也倾向于认同在initialize方法内调用render是一种不好的实践,因为它会阻止你在不想立即渲染的情况下更加高效。你对此有什么看法? - Ian Storm Taylor
5
不能重新渲染 ParentView 是一个很大的限制。我有一个情况,基本上是一个选项卡控件,每个选项卡显示一个不同的 ParentView。当第二次点击选项卡时,我想重新渲染现有的 ParentView,但这样做会导致 ChildViews 中的事件丢失(我在 init 中创建子级,在 render 中呈现)。我猜要么 1) 隐藏/显示而不是重新渲染,2) 在 ChildViews 的 render 中委托事件,3) 在 render 中创建子级,或者 4) 在性能成为问题之前不必担心,并每次重新创建 ParentView。 - Paul Hoenecke
非常感谢您的精彩解释。我有两个问题,处理子视图时使用Marionette是否是更明智的决定?另外,如果我们从父视图传递一个集合,并希望在集合重置时重新呈现子视图,该如何绑定子视图的事件? - aliirz
1
你如何将集合传递给子PhoneListView - Tri Nguyen
@IanStormTaylor @KevinPeel — 把 this.trigger( 'render' ); 放在父视图的 render 中怎么样?然后在创建子视图时传递父视图:new InfoView({ parent: this, /*...*/ });。最后,在子视图的 initialize 中:this.listenTo( arguments.options.parent, 'render', this.render ); - J.D.
显示剩余4条评论

6
对我来说,通过某种标志区分视图的初始设置和后续设置似乎不是世界上最糟糕的想法。为了使其简洁易懂,应将该标志添加到您自己的视图中,该视图应扩展Backbone(Base)视图。
与Derick一样,我不完全确定这是否直接回答了您的问题,但我认为在这种情况下至少值得提及。
“另请参见:在Backbone中使用Eventbus”Also see: Use of an Eventbus in Backbone

6

4

Kevin Peel提供了很好的答案-这是我简洁的版本:

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)

当容器进行渲染时,使用'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 结构。Container 和 Children 仍然会生成自己的 DOM 元素,并且只生成一次。


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

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

这个插件的方法是在父视图首次渲染后创建和呈现子视图。然后,在父视图的后续渲染中,$.detach子视图元素,重新渲染父视图,然后将子视图元素插入到适当的位置并重新呈现它们。这样,在后续的渲染中可以重复使用子视图对象,而无需重新委托事件。
请注意,集合视图的情况(其中集合中的每个模型都用一个子视图表示)非常不同,并且需要自己讨论/解决。我认为最好的通用解决方案是Marionette中的CollectionView
编辑:对于集合视图的情况,如果您需要基于单击和/或拖放选择模型以进行重新排序,则还可以查看这种更注重UI实现

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接