骨干僵尸视图和良好做法 [英] Backbone zombie views & good practice

查看:24
本文介绍了骨干僵尸视图和良好做法的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我对主干还很陌生,我试图了解僵尸视图的来龙去脉.

僵尸是,根据这个 文章:

<块引用>

当我们通过事件将对象绑定在一起但我们不去解绑它们时.只要这些对象绑定在一起,并且在我们的应用程序代码中至少引用了其中之一,它们就不会被清理或垃圾收集.由此产生的内存泄漏就像电影中的僵尸——躲在黑暗的角落里,等着跳出来吃我们的午餐.

上面提到的文章建议创建一个对象来管理视图之间的转换,然后实现一个关闭函数来移除和解除绑定视图.

话虽如此,根据情况,从哪里调用关闭函数?

我在父视图的初始化块中添加了一个属性来跟踪子视图.这样我就可以在用新的替换它之前调用 .remove() .这是好的做法还是有更好的方法?

我也不明白为什么定义el然后用

渲染

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

不允许我在视图按预期工作时解除绑定

$('#sportsManDetails').html(this.$el.html(this.template(this.model.attributes)));

至于示例,我刚刚创建了一个简单的应用程序,该应用程序显示运动员姓名列表,并在单击姓名时显示更多详细信息.

这是代码和一个有效的fiddle:

html

<script id="sportsManDetailsTemplate" type="text/template"><ul><li><%=第一%></li><li><%=最后的%></li><li><%=年龄%></li><li><%=运动%></li><li><%=类别%></li><button class="test">Test</button><div id="sportsMenName"></div><div id="sportsManDetails"></div>

JS

模型和集合

var app = app ||{};app.SportsManModel = Backbone.Model.extend({});app.SportsMenCollection = Backbone.Collection.extend({模型:app.SportsManModel});

名称视图

app.NameView = Backbone.View.extend({tagName: 'li',className: 'sportsMan',模板:_.template($('#nameListTemplate').html()),初始化:函数(){this.sportsManDetailsView;},事件:{'click': 'showSportsManDetails'},showSportsManDetails: 函数(e){if (typeof this.sportsManDetailsView !=='未定义'){this.sportsManDetailsView.remove();}this.sportsManDetailsView = 新 app.SportsManDetailsView({模型:this.model})},渲染:函数(){this.$el.append(this.template(this.model.attributes));返回这个;}});

NameListView

app.NameListView = Backbone.View.extend({el: '#sportsMenName',初始化:函数(体育人){this.collection = new app.SportsMenCollection(sportsMen);this.render();},渲染:函数(){this.collection.each(function(sportsMen){this.renderContact(sportsMen);}, 这);},renderContact: 函数(sportsMen){var nameView = new app.NameView({型号: 运动男});this.$el.append(nameView.render().el);}});

SportsManDetailsView

app.SportsManDetailsView = Backbone.View.extend({//如果我将 el 与//this.$el.html(this.template(this.model.attributes));//el: '#sportsManDetails',模板:_.template($('#sportsManDetailsTemplate').html()),初始化:函数(){this.render();},事件:{'点击 .test': '测试'},测试:函数(){警报('测试');},渲染:函数(){//这不起作用//this.$el.html(this.template(this.model.attributes));//这是好的做法吗?$('#sportsManDetails').html(this.$el.html(this.template(this.model.attributes)));}});

app.js

varsportsMen = [{first:'Quentin',最后:'Tarant',年龄:'34',运动:'bike',类别:'- 90kg'},{first:'Aymeric',最后:'McArthur',年龄:'54',运动:'jetski',类别:'200HP'},{第一个:'彼得',最后一个:'TheFat',年龄:'45',运动:'冰壶',类别:'不知道'},{first:'Charles',最后:'Martel',年龄:'21',运动:'Moto',类别:'MX 250cc'},];$(函数(){新 app.NameListView(sportsMen);});

解决方案

正如您所发现的,Backbone 认为自己更像是一个而不是一个框架——它留下了很多问题和设计模式留给开发人员.

术语僵尸视图"用于指定当您认为它们已死时仍然绑定到某物(因此还活着)的视图.通常会有一个来自 model.on call 或类似的视图的剩余引用.基本上是内存泄漏的一种特定形式.

要管理视图的生命周期,您可以使用父视图,但从路由器执行此操作是一种常见做法.路由器用于在路由事件时删除旧视图并实例化新视图.以下是我通常如何做到这一点的片段:

render: function(){this.mainView &&this.mainView.remove();//如果已经有一个视图,删除它this.mainView = new SomeOtherKindOfViewDeterminedBySomeEvent();//实例化新视图this.mainView.render();this.mainView.$el.appendTo('#main-content');//附加它}

注意事项:

  1. 如果不对视图显式调用 remove,您的应用将容易受到内存泄漏的影响.这是因为 View 的事件和属性仍然存在于后台.例如,如果您删除上面示例的第一行,我将丢失对以前的 this.mainView 的引用,但它的事件仍在使用内存.随着时间的推移,这将对您的应用产生影响.
  2. 请注意,我在最后一行使用了 appendTo.当在视图上调用 remove 时,它的整个元素以及它的事件都会被删除.如果我只是这样做:

    this.mainView = new SomeOtherKindOfViewDeterminedBySomeEvent({ el: '#main-content' })

    然后在我对 this.mainView 调用 remove 之后,#main-content 将从 DOM 中删除,所以我不能不再使用该选择器.通过附加它,我将 #main-content 作为占位符保留在周围,因此我可以继续向其附加视图.这是您在尝试解除绑定 SportsManDetailsView 然后再次渲染时所看到的.

至于你的问题,这个:

$('#sportsManDetails').html(this.$el.html(this.template(this.model.attributes)));

不是好的做法.首先是您使用了全局 jQuery 对象,这与 Backbone 的封装视图方法相悖.其次,事件在以前的视图中仍然活跃在 DOM 中,导致内存泄漏.您可以在单击测试"按钮时看到这一点 - 每次实例化 SportsManDetailsView 时都会触发处理函数(第二次,警报消息将显示两次,然后显示三次,以此类推.)

您应该依靠父视图或路由器来处理此类交互.或者将您的 SportsManDetailsView 绑定到 #sportsManDetails 元素,并且永远不要删除它.然后当您的 NameView 中发生点击事件时,让它的模型触发器触发一个事件.然后您的 SportsManDetailsView 可以监听相应集合中的事件并相应地重新渲染自身.拥抱 Backbone 的事件! JavaScript 是一种事件驱动的语言,永远不要忘记您的大炮中有这些.

我已经更新了您的 JSFiddle 以展示我所讨论的一些内容.>

I'm fairly new to backbone and I try to understand the ins and outs of zombie views.

A zombie is, according to this article:

When we bind objects together through events but we don’t bother unbinding them. As long as these objects are bound together, and there is a reference in our app code to at least one of them, they won’t be cleaned up or garbage collected. The resulting memory leaks are like the zombies of the movies – hiding in dark corners, waiting to jump out and eat us for lunch.

The article mentionned above suggests to create an object that manages the transitions between views and then to implement a close function to remove and unbind the view.

That being said, depending on the situation, where to call that close function from?

I add a property in the initialize block of my parent view to keep a trace of the child view. That way I'm able to call .remove() on it before I replace it by a new one. Is it good practice or is there a better way?

I also don't understand why defining el and then rendering with

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

doesn't allow me to unbind the view while it works as expected by doing

$('#sportsManDetails').html(this.$el.html(this.template(this.model.attributes)));

As for the exemple, I just created a simple app that displays a list of sportsmen's names and that shows more details when clicking on a name.

Here's the code and a working fiddle:

html

<script id="nameListTemplate" type="text/template">
    <%= first %> <%= last %>
</script>
<script id="sportsManDetailsTemplate" type="text/template">
    <ul>
        <li><%= first %></li>
        <li><%= last %></li>
        <li><%= age %></li>
        <li><%= sport %></li>
        <li><%= category %></li>
    </ul>
    <button class="test">Test</button>
</script>
<div id="sportsMenName"></div>
<div id="sportsManDetails"></div>

JS

model and collection

var app = app || {};

app.SportsManModel = Backbone.Model.extend({});

app.SportsMenCollection = Backbone.Collection.extend({
    model: app.SportsManModel
});

NameView

app.NameView = Backbone.View.extend({
    tagName: 'li',
    className: 'sportsMan',
    template: _.template($('#nameListTemplate').html()),

    initialize: function(){
        this.sportsManDetailsView;  
    },

    events: {
        'click': 'showSportsManDetails'
    },

    showSportsManDetails: function(e){
        if (typeof this.sportsManDetailsView !== 'undefined'){
            this.sportsManDetailsView.remove();
        }
        this.sportsManDetailsView = new app.SportsManDetailsView({
            model: this.model
        })  
    },

    render: function(){
        this.$el.append(this.template(this.model.attributes));
        return this;
    }
});

NameListView

app.NameListView = Backbone.View.extend({
    el: '#sportsMenName',

    initialize: function(sportsMen){
        this.collection = new app.SportsMenCollection(sportsMen);
        this.render();
    },

    render: function(){
        this.collection.each(function(sportsMen){
            this.renderContact(sportsMen);
        }, this);
    },

    renderContact: function(sportsMen){
        var nameView = new app.NameView({
            model: sportsMen   
        });
        this.$el.append(nameView.render().el);
    }
});

SportsManDetailsView

app.SportsManDetailsView = Backbone.View.extend({
    // doesn't work if I use el in conjunction with 
    // this.$el.html(this.template(this.model.attributes));
    // el: '#sportsManDetails',
    template: _.template($('#sportsManDetailsTemplate').html()),

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

    events: {
        'click .test': 'test'
    },

    test: function(){
        alert('test');  
    },

    render: function(){                      
        // that does not work
        //this.$el.html(this.template(this.model.attributes));

        // is this good practice?
        $('#sportsManDetails').html(this.$el.html(this.template(this.model.attributes)));
    }
});

app.js

var sportsMen = [
    {first: 'Quentin', last: 'Tarant', age: '34', sport: 'bike', category: '- 90kg'},
    {first: 'Aymeric', last: 'McArthur', age: '54', sport: 'jetski', category: '200HP'},
    {first: 'Peter', last: 'TheFat', age: '45', sport: 'curling', category: 'dunno'},
    {first: 'Charles', last: 'Martel', age: '21', sport: 'Moto', category: 'MX 250cc'},
];

$(function(){
    new app.NameListView(sportsMen);
});

解决方案

Just as you're discovering, Backbone considers itself more of a library than a framework - it leaves a lot of questions and design patterns left to the developer.

The term "zombie view" is used to designate views that are still bound to something (and thus alive) when you think they're dead. Usually there's a leftover reference to the view from a model.on call or similar. Basically a specific form of memory leak.

To manage the life cycle of the view, you can use a parent view, but it is a common practice to do this from a router. The router is used to remove old views and instantiate new ones upon a route event. Here's a snippet of how I often accomplish this:

render: function(){
    this.mainView && this.mainView.remove();                    // if there is already a view, remove it
    this.mainView = new SomeOtherKindOfViewDeterminedBySomeEvent(); // instantiate the new view
    this.mainView.render();
    this.mainView.$el.appendTo( '#main-content' );              // append it
}

Some things to note:

  1. Without explicitly calling remove on a view, your app will be vulnerable to memory leaks. This is because the View's events and properties still exist in the background. For example, if you remove the first line of the example above, I will lose my reference to the former this.mainView, but it's events are still using memory. This will have an effect on your app over time.
  2. Note that I'm using appendTo in the last line. When calling remove on a View, it's entire element is removed, as well as it's events. If I had simply done this:

    this.mainView = new SomeOtherKindOfViewDeterminedBySomeEvent({ el: '#main-content' })

    Then after I call remove on this.mainView, #main-content will have been removed from the DOM, so I can no longer use that selector. By appending it, I keep that #main-content around as a placeholder, so I can continue to append views to it. This is what you are seeing when trying to unbind SportsManDetailsView then render it again.

As for your questions, this:

$('#sportsManDetails').html(this.$el.html(this.template(this.model.attributes)));

Is not good practice. This first is that you've used the global jQuery object, which defeats Backbone's approach of encapsulated views. Second, the events are still active in the DOM from former views, leading to memory leaks. You can see this when you click the Test button - the handler function will fire for every time you instantiated a SportsManDetailsView (the second time around, the alert message will be shown twice, then three times, etc.)

You should rely on a parent view or router to handle such interaction. That, or keep your SportsManDetailsView bound to the #sportsManDetails element, and never remove it. Then when the click event occurs in your NameView, have its model trigger trigger an event. Then your SportsManDetailsView can listen for the event in the corresponding collection and re-render itself accordingly. Embrace Backbone's events! JavaScript is an event-driven language, and never forget that you have those in your artillery.

I have updated your JSFiddle to demonstrate some of what I've talked about.

这篇关于骨干僵尸视图和良好做法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆