如何在Backbone.js中呈现和附加子视图


133

我有一个嵌套视图设置,可以在我的应用程序中深入了解。我可以想到很多初始化,渲染和附加子视图的方法,但是我想知道什么是常见的做法。

这是我想到的几个:

initialize : function () {

    this.subView1 = new Subview({options});
    this.subView2 = new Subview({options});
},

render : function () {

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

    this.subView1.setElement('.some-el').render();
    this.subView2.setElement('.some-el').render();
}

优点:您不必担心通过追加来维护正确的DOM顺序。视图是在很早的时候初始化的,因此在render函数中没有太多事情可以一次完成。

缺点:您被迫重新委托delegateEvents(),这可能会很昂贵?父视图的渲染功能杂乱无章,需要进行所有子视图渲染?您没有设置tagName元素的能力,因此模板需要维护正确的tagName。

其他方式:

initialize : function () {

},

render : function () {

    this.$el.empty();

    this.subView1 = new Subview({options});
    this.subView2 = new Subview({options});

    this.$el.append(this.subView1.render().el, this.subView2.render().el);
}

优点:您不必重新委派活动。您不需要仅包含空占位符的模板,并且您的tagName可以重新由视图定义。

缺点:您现在必须确保以正确的顺序附加内容。子视图渲染仍然使父视图的渲染混乱。

随着onRender事件:

initialize : function () {
    this.on('render', this.onRender);
    this.subView1 = new Subview({options});
    this.subView2 = new Subview({options});
},

render : function () {

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

    //other stuff

    return this.trigger('render');
},

onRender : function () {

    this.subView1.setElement('.some-el').render();
    this.subView2.setElement('.some-el').render();
}

优点:子视图逻辑现在与视图的render()方法分开了。

随着onRender事件:

initialize : function () {
    this.on('render', this.onRender);
},

render : function () {

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

    //other stuff

    return this.trigger('render');
},

onRender : function () {
    this.subView1 = new Subview();
    this.subView2 = new Subview();
    this.subView1.setElement('.some-el').render();
    this.subView2.setElement('.some-el').render();
}

我在所有这些示例中都混用了一些不同的做法(对此感到抱歉),但是您会保留或添加哪些呢?而你不会做什么?

实践摘要:

  • initialize或中实例化子视图render
  • 在中render或中执行所有子视图渲染逻辑onRender
  • 使用setElement还是append/appendTo

我会小心新的而不删除它,那里内存泄漏。
vimdude '02

1
不用担心,我有一个清理孩子的close方法和一个对象onClose,但是我只是好奇如何首先实例化和渲染它们。
伊恩·风暴泰勒

3
@abdelsaid:在JavaScript中,GC处理内存的重新分配。deleteJS中的代码与deleteC ++中的代码不同。如果您问我,这是一个名称很差的关键字。
迈克·贝利

@MikeBantegui做到了,但它与Java中的相同,只是在JS中释放内存只需要分配null。为了弄清楚我的意思,请尝试使用内部新对象创建一个循环并监视内存。当然,GC可以解决这个问题,但是您将失去内存,然后再解决它。在这种情况下,Render可能会被调用很多次。
vimdude 2012年

3
我是Backbone新手开发人员。有人可以解释为什么示例1迫使我们重新委托事件吗?(或者我应该问这个问题吗?)谢谢。
pilau

Answers:


58

我通常看到/使用了几种不同的解决方案:

解决方案1

var OuterView = Backbone.View.extend({
    initialize: function() {
        this.inner = new InnerView();
    },

    render: function() {
        this.$el.html(template); // or this.$el.empty() if you have no template
        this.$el.append(this.inner.$el);
        this.inner.render();
    }
});

var InnerView = Backbone.View.extend({
    render: function() {
        this.$el.html(template);
        this.delegateEvents();
    }
});

这类似于您的第一个示例,但有一些更改:

  1. 附加子元素的顺序很重要
  2. 外部视图不包含要在内部视图上设置的html元素(这意味着您仍可以在内部视图中指定tagName)
  3. render()被调用后内视图的元素已经被放置到DOM,如果你内心的看法的是有帮助的render()方法是拨打/基于其他元素的位置/大小(这是一种常见的情况,在我的经验),在页面上浆纱本身

解决方案2

var OuterView = Backbone.View.extend({
    initialize: function() {
        this.render();
    },

    render: function() {
        this.$el.html(template); // or this.$el.empty() if you have no template
        this.inner = new InnerView();
        this.$el.append(this.inner.$el);
    }
});

var InnerView = Backbone.View.extend({
    initialize: function() {
        this.render();
    },

    render: function() {
        this.$el.html(template);
    }
});

解决方案2看起来更干净,但是它在我的经验中引起了一些奇怪的事情,并且对性能产生了负面影响。

我通常使用解决方案1,原因有两个:

  1. 我的很多观点都依赖于其render()方法中已经存在的DOM
  2. 重新渲染外部视图时,不必重新初始化视图,这种重新初始化可能会导致内存泄漏,并导致现有绑定出现怪异问题

请记住,如果要初始化new View()一次render(),则delegateEvents()无论如何都会调用该初始化。因此,正如您所表达的,这不一定是“骗局”。


1
这些解决方案都无法处理调用View.remove的子视图树,这对于在视图中进行自定义清理可能至关重要,否则将阻止垃圾回收
Dominic

31

对于Backbone来说,这是一个长期存在的问题,以我的经验,这个问题并没有一个令人满意的答案。我与您一样感到沮丧,尤其是尽管本使用案例非常普遍,但指导却很少。也就是说,我通常会采用类似于您的第二个示例的方法。

首先,我会随意删除任何需要您重新委托事件的内容。Backbone的事件驱动视图模型是其最关键的组件之一,仅由于您的应用程序不平凡就失去该功能将在任何程序员的嘴中留下不良的味道。所以从头开始。

关于第三个示例,我认为这只是围绕常规渲染实践的最终尝试,并没有增加太多意义。也许如果您正在执行实际的事件触发(即,不是人为的“ onRender”事件),则仅将这些事件绑定到render自身就值得。如果发现render笨拙和复杂,则子视图太少。

回到您的第二个示例,这可能是三个弊端中的较小者。这是在我的PDF版本的第42页上从“ 带有主干的食谱”中提取的示例代码:

...
render: function() {
    $(this.el).html(this.template());
    this.addAll();
    return this;
},
  addAll: function() {
    this.collection.each(this.addOne);
},
  addOne: function(model) {
    view = new Views.Appointment({model: model});
    view.render();
    $(this.el).append(view.el);
    model.bind('remove', view.remove);
}

这只是比第二个示例稍微复杂一点的设置:它们指定了一组函数addAlladdOne来完成繁琐的工作。我认为这种方法是可行的(并且我当然会使用);但它仍然留下奇怪的回味。(请原谅所有这些舌头隐喻。)

关于以正确的顺序进行添加的观点:如果严格地添加,那么这是一个限制。但是请确保考虑所有可能的模板方案。也许您实际上想要一个占位符元素(例如empty divul),然后可以replaceWith一个新的(DOM)元素来保存适当的子视图。追加不是唯一的解决方案,如果您非常关心订购问题,那么您当然可以解决它,但是我想如果您遇到麻烦,就会遇到设计问题。记住,子视图可以有子视图,如果合适的话,也应该有子视图。这样,您将拥有一个类似树的结构,这非常好:每个子视图依次添加其所有子视图,然后在父视图添加另一个子视图,依此类推。

不幸的是,使用开箱即用的骨干网,解决方案2可能是最好的。如果您有兴趣签出第三方库,我曾研究过(但实际上还没有时间玩)的第三方库是Backbone.LayoutManager,它似乎有一种更健康的添加子视图的方法。但是,即使他们最近也曾就类似的问题进行过辩论


4
倒数第二行-- model.bind('remove', view.remove);您难道不应该在约会的initialize函数中这样做以使它们分开吗?
2012年

2
如果视图由于保持状态而无法在每次父视图呈现时重新实例化怎么办?
2013年

停止所有这些疯狂行为,只需使用Backbone.subviews插件即可!
勇敢的戴夫2014年

6

奇怪的是尚未提及,但我会认真考虑使用Marionette

它强制多一点结构主干的应用程序,包括特定视图类型(ListViewItemViewRegionLayout),加入适量的ControllerS和更大量。

这是有关Github的项目,以及Addy Osmani撰写的《 Backbone Fundamentals》入门指南。


3
这不能回答问题。
Ceasar Bautista 2015年

2
@CeasarBautista我不讨论如何使用木偶来完成此操作,但木偶确实可以解决上述问题
Dana Woodman

4

我相信,对于这个问题,我有一个非常全面的解决方案。它允许更改集合中的模型,并且仅重新渲染其视图(而不是整个集合)。它还通过close()方法处理僵尸视图的移除。

var SubView = Backbone.View.extend({
    // tagName: must be implemented
    // className: must be implemented
    // template: must be implemented

    initialize: function() {
        this.model.on("change", this.render, this);
        this.model.on("close", this.close, this);
    },

    render: function(options) {
        console.log("rendering subview for",this.model.get("name"));
        var defaultOptions = {};
        options = typeof options === "object" ? $.extend(true, defaultOptions, options) : defaultOptions;
        this.$el.html(this.template({model: this.model.toJSON(), options: options})).fadeIn("fast");
        return this;
    },

    close: function() {
        console.log("closing subview for",this.model.get("name"));
        this.model.off("change", this.render, this);
        this.model.off("close", this.close, this);
        this.remove();
    }
});
var ViewCollection = Backbone.View.extend({
    // el: must be implemented
    // subViewClass: must be implemented

    initialize: function() {
        var self = this;
        self.collection.on("add", self.addSubView, self);
        self.collection.on("remove", self.removeSubView, self);
        self.collection.on("reset", self.reset, self);
        self.collection.on("closeAll", self.closeAll, self);
        self.collection.reset = function(models, options) {
            self.closeAll();
            Backbone.Collection.prototype.reset.call(this, models, options);
        };
        self.reset();
    },

    reset: function() {
        this.$el.empty();
        this.render();
    },

    render: function() {
        console.log("rendering viewcollection for",this.collection.models);
        var self = this;
        self.collection.each(function(model) {
            self.addSubView(model);
        });
        return self;
    },

    addSubView: function(model) {
        var sv = new this.subViewClass({model: model});
        this.$el.append(sv.render().el);
    },

    removeSubView: function(model) {
        model.trigger("close");
    },

    closeAll: function() {
        this.collection.each(function(model) {
            model.trigger("close");
        });
    }
});

用法:

var PartView = SubView.extend({
    tagName: "tr",
    className: "part",
    template: _.template($("#part-row-template").html())
});

var PartListView = ViewCollection.extend({
    el: $("table#parts"),
    subViewClass: PartView
});


0

我真的不喜欢上述任何解决方案。对于需要手动在render方法中工作的每个视图,我更喜欢这种配置。

  • views 可以是返回视图定义对象的函数或对象
  • 当父母的.remove被叫.remove子级时,应调用从最低顺序开始的嵌套子级的子级(从子子子视图一直到全部)
  • 默认情况下,父视图会传递其自己的模型和集合,但是可以添加和覆盖选项。

这是一个例子:

views: {
    '.js-toolbar-left': CancelBtnView, // shorthand
    '.js-toolbar-right': {
        view: DoneBtnView,
        append: true
    },
    '.js-notification': {
        view: Notification.View,
        options: function() { // Options passed when instantiating
            return {
                message: this.state.get('notificationMessage'),
                state: 'information'
            };
        }
    }
}

0

骨干是有意建造的,因此在此问题和许多其他问题上没有“常见”的做法。它意味着要尽可能不受质疑。从理论上讲,您甚至不必在Backbone中使用模板。您可以在render视图功能中使用javascript / jquery 来手动更改视图中的所有数据。为了使其更加极端,您甚至不需要一项特定render功能。您可以使用一个名为的函数renderFirstName来更新dom和renderLastName更新的名字。如果您采用这种方法,那么在性能方面会更好,并且您将不必再次手动委派事件。该代码对于阅读它的人也将是完全有意义的(尽管这将是更长/更混乱的代码)。

但是,通常没有使用模板,仅销毁和重建整个视图以及每个渲染调用的子视图的弊端,因为发问者根本没有做其他任何事情。这就是大多数人在遇到的每种情况下几乎都要做的事情。这就是为什么有思想的框架只是将其设置为默认行为。


0

您还可以将渲染的子视图作为变量注入到主模板中作为变量。

首先呈现子视图,并将其转换为html,如下所示:

var subview1 = $(subview1.render.el).html(); var subview2 = $(subview2.render.el).html();

(这样一来,您还可以动态地字符串连接视图,例如 subview1 + subview2在循环中使用时),然后将其传递给主模板,如下所示: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

并最终像这样注入它:

this.$el.html(_.template(MasterTemplate, { sub1: subview1, sub2: subview2 } ));

关于子视图中的事件:使用这种方法很可能必须将它们连接到父视图(masterView)中,而不是子视图中。


0

我喜欢使用以下方法,该方法还可以确保正确删除子视图。这是Addy Osmani 的书中的一个示例。

Backbone.View.prototype.close = function() {
    if (this.onClose) {
        this.onClose();
    }
    this.remove(); };

NewView = Backbone.View.extend({
    initialize: function() {
       this.childViews = [];
    },
    renderChildren: function(item) {
        var itemView = new NewChildView({ model: item });
        $(this.el).prepend(itemView.render());
        this.childViews.push(itemView);
    },
    onClose: function() {
      _(this.childViews).each(function(view) {
        view.close();
      });
    } });

NewChildView = Backbone.View.extend({
    tagName: 'li',
    render: function() {
    } });

0

不需要重新委派事件,因为这代价很高。见下文:

    var OuterView = Backbone.View.extend({
    initialize: function() {
        this.inner = new InnerView();
    },

    render: function() {
        // first detach subviews            
        this.inner.$el.detach(); 

        // now can set html without affecting subview element's events
        this.$el.html(template);

        // now render and attach subview OR can even replace placeholder 
        // elements in template with the rendered subview element
        this.$el.append(this.inner.render().el);

    }
});

var InnerView = Backbone.View.extend({
    render: function() {
        this.$el.html(template);            
    }
});
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.