Backbone.js:重新填充或重新创建视图?

83
在我的Web应用程序中,我在左侧的表格中有一个用户列表,并在右侧有一个用户详细信息面板。当管理员点击表格中的用户时,其详细信息应显示在右侧。
我在左侧有一个UserListView和UserRowView,右侧有一个UserDetailView。一切似乎都正常工作,但是我遇到了奇怪的行为。如果我先单击左侧的一些用户,然后单击其中一个用户的删除按钮,那么我会得到连续的JavaScript确认框,这些框显示所有已经显示过的用户。
看起来之前显示的所有视图的事件绑定没有被删除,这似乎很正常。我不应该每次在UserRowView上创建一个新的UserDetailView吗?我应该维护一个视图并更改其引用模型吗?我应该跟踪当前视图并在创建新视图之前删除它吗?我有点迷茫,欢迎任何想法。谢谢!
这是左侧视图的代码(行显示,单击事件,右侧视图创建):
window.UserRowView = Backbone.View.extend({
    tagName : "tr",
    events : {
        "click" : "click",
    },
    render : function() {
        $(this.el).html(ich.bbViewUserTr(this.model.toJSON()));
        return this;
    },
    click : function() {
        var view = new UserDetailView({model:this.model})
        view.render()
    }
})

这是右侧视图(删除按钮)的代码

window.UserDetailView = Backbone.View.extend({
    el : $("#bbBoxUserDetail"),
    events : {
        "click .delete" : "deleteUser"
    },
    initialize : function() {
        this.model.bind('destroy', function(){this.el.hide()}, this);
    },
    render : function() {
        this.el.html(ich.bbViewUserDetail(this.model.toJSON()));
        this.el.show();
    },
    deleteUser : function() {
        if (confirm("Really delete user " + this.model.get("login") + "?")) 
            this.model.destroy();
        return false;
    }
})
7个回答

136

我总是销毁并创建视图,因为随着我的单页面应用程序越来越大,保留未使用的活动视图以便重复使用将变得难以维护。

这里是我使用的一种简化技术,用于清理我的视图以避免内存泄漏。

我首先创建一个所有视图都继承的BaseView。基本思想是,我的视图将保持对其订阅的所有事件的引用,因此在处理视图的时候,所有这些绑定都会自动解除绑定。以下是我的BaseView的示例实现:

var BaseView = function (options) {

    this.bindings = [];
    Backbone.View.apply(this, [options]);
};

_.extend(BaseView.prototype, Backbone.View.prototype, {

    bindTo: function (model, ev, callback) {

        model.bind(ev, callback, this);
        this.bindings.push({ model: model, ev: ev, callback: callback });
    },

    unbindFromAll: function () {
        _.each(this.bindings, function (binding) {
            binding.model.unbind(binding.ev, binding.callback);
        });
        this.bindings = [];
    },

    dispose: function () {
        this.unbindFromAll(); // Will unbind all events this view has bound to
        this.unbind();        // This will unbind all listeners to events from 
                              // this view. This is probably not necessary 
                              // because this view will be garbage collected.
        this.remove(); // Uses the default Backbone.View.remove() method which
                       // removes this.el from the DOM and removes DOM events.
    }

});

BaseView.extend = Backbone.View.extend;

每当一个视图需要绑定到模型或集合上的事件时,我会使用bindTo方法。例如:

var SampleView = BaseView.extend({

    initialize: function(){
        this.bindTo(this.model, 'change', this.render);
        this.bindTo(this.collection, 'reset', this.doSomething);
    }
});
无论何时我删除一个视图,我只需调用dispose方法即可自动清理所有内容。
var sampleView = new SampleView({model: some_model, collection: some_collection});
sampleView.dispose();

我与写作《Backbone.js on Rails》电子书的人分享了这个技巧,我相信他们已经采用了这种技巧。

更新:2014-03-24

从Backone 0.9.9版本开始,使用与上面展示的bindTo和unbindFromAll技巧相同的方式添加了listenTo和stopListening到事件中。此外,View.remove自动调用stopListening,因此现在绑定和解除绑定就像这样简单:

var SampleView = BaseView.extend({

    initialize: function(){
        this.listenTo(this.model, 'change', this.render);
    }
});

var sampleView = new SampleView({model: some_model});
sampleView.remove();

你有没有关于如何处理嵌套视图的建议?目前我正在做类似于bindTo:https://gist.github.com/1288947 的操作,但我想可能有更好的方法。 - Dmitry Polushkin
Dmitry,我做的事情与你处理嵌套视图的方式类似。 我还没有看到更好的解决方案,但如果有的话,我也很感兴趣了解一下。 这里还有另一个讨论也涉及到此问题:https://groups.google.com/forum/#!topic/backbonejs/3ZFm-lteN-A。 我注意到在你的解决方案中,你没有考虑到直接处理嵌套视图的场景。 在这种情况下,即使已处理嵌套视图,父视图仍将持有对其的引用。 我不知道你是否需要考虑这个问题。 - Johnny Oshika
如果我有一个打开和关闭同一视图的功能,我有前进和后退按钮。如果我调用dispose,它将从DOM中删除该元素。我应该一直保留视图在内存中吗? - dagda1
1
嗨,fisherwebdev。你也可以使用Backbone.View.extend来使用这种技术,但你需要在BaseView.initialize方法中初始化this.bindings。问题在于,如果你继承的视图实现了自己的initialize方法,那么它就需要显式调用BaseView的initialize方法。我在这里更详细地解释了这个问题:https://dev59.com/fGsz5IYBdhLWcg3wrJ2-#7736030 - Johnny Oshika
2
嗨,SunnyRed,我更新了我的答案,以更好地反映我销毁视图的原因。使用Backbone,我认为在应用程序启动后永远不需要重新加载页面,因此我的单页应用程序变得非常大。当用户与我的应用程序交互时,我不断重新呈现页面的不同部分(例如从详细视图切换到编辑视图),因此我发现始终创建新视图要容易得多,而不管该部分以前是否呈现过。另一方面,模型代表业务对象,因此只有在对象真正改变时才会修改它们。 - Johnny Oshika
显示剩余8条评论

28

1
为什么不在路由器中直接使用 delete view 呢? - Trantor Liu
我已经为你的答案点赞了,但是如果将博客文章中相关部分放入答案中,这将对此处的目标非常有益。 - Emile Bergeron

8

这是一个常见的问题。如果您每次创建新视图,所有旧视图仍将绑定到所有事件。您可以在视图上创建一个名为detatch的函数,以解决这个问题:

detatch: function() {
   $(this.el).unbind();
   this.model.unbind();

在创建新视图之前,请确保在旧视图上调用detatch

当然,如您所提到的,您始终可以创建一个“详细”视图并永远不更改它。您可以将模型的“更改”事件(来自视图)绑定到自己的重新渲染上。将其添加到您的初始化器中:

this.model.bind('change', this.render)

这样做会导致详细信息面板每次更改模型时重新渲染。您可以通过观察单个属性“change:propName”来获得更细的粒度。

当然,这需要一个公共模型,该项视图以及更高级别的列表视图和详细视图引用。

希望这有所帮助!


1
嗯,我按照你建议的方向做了一些事情,但我仍然有问题:例如,this.model.unbind() 对我来说是错误的,因为它解除了与此模型相关的所有事件,包括同一用户的其他视图的事件。此外,为了调用 detach 函数,我需要保留对视图的静态引用,而我并不太喜欢这样做。我怀疑还有一些东西我没有理解... - solendil

6
为了修复多次绑定事件的问题,请按以下步骤操作:
$("#my_app_container").unbind()
//Instantiate your views here

在从路由创建新的视图之前使用上述代码可以解决我遇到的关于“僵尸视图”的问题。

这里有很多非常好的、详细的答案。我一定会研究一些ViewManger的建议。然而,这个方法非常简单,对我来说完美地解决了问题,因为我的视图都是带有close()方法的面板,在那里我可以解除事件绑定。谢谢Ashan。 - netpoetica
2
我似乎无法在解绑后重新渲染:\ - CodeGuru
@FlyingAtom:即使我解除绑定后也无法重新渲染视图。你找到任何方法了吗? - Raeesaa
view.$el.removeData().unbind(); - Alexander Mills

2

我认为大多数使用Backbone的人会像您的代码一样创建视图:

var view = new UserDetailView({model:this.model});

这段代码创建了“僵尸视图”,因为我们可能会不断地创建新的视图而不清理现有的视图。然而,在应用程序中调用view.dispose()清理所有Backbone视图并不方便(特别是如果我们在for循环中创建视图)。

我认为最好的时机是在创建新视图之前放置清理代码。我的解决方案是创建一个帮助程序来进行清理:

window.VM = window.VM || {};
VM.views = VM.views || {};
VM.createView = function(name, callback) {
    if (typeof VM.views[name] !== 'undefined') {
        // Cleanup view
        // Remove all of the view's delegated events
        VM.views[name].undelegateEvents();
        // Remove view from the DOM
        VM.views[name].remove();
        // Removes all callbacks on view
        VM.views[name].off();

        if (typeof VM.views[name].close === 'function') {
            VM.views[name].close();
        }
    }
    VM.views[name] = callback();
    return VM.views[name];
}

VM.reuseView = function(name, callback) {
    if (typeof VM.views[name] !== 'undefined') {
        return VM.views[name];
    }

    VM.views[name] = callback();
    return VM.views[name];
}

使用虚拟机创建视图可以帮助清理现有的视图,而不必调用view.dispose()。您可以对代码进行小修改,从中受益。
var view = new UserDetailView({model:this.model});

to

var view = VM.createView("unique_view_name", function() {
                return new UserDetailView({model:this.model});
           });

所以,如果视图干净的话,您可以选择重用视图而不是不断地创建它。只需将“createView”更改为“reuseView”。
var view = VM.reuseView("unique_view_name", function() {
                return new UserDetailView({model:this.model});
           });

可在https://github.com/thomasdao/Backbone-View-Manager上查看详细的代码和归属。


最近我一直在深入研究Backbone,并且这似乎是处理构建或重用视图时最完善的处理僵尸视图的方法。通常我会遵循Derick Bailey的示例,但在这种情况下,这似乎更加灵活。 我的问题是,为什么没有更多人使用这种技术呢? - MFD3000
也许是因为他很擅长使用Backbone :)。我认为这种技术相当简单且安全,我一直在使用它,到目前为止没有问题 :)。 - thomasdao

0

一种替代方案是绑定,而不是创建一系列新视图,然后解除这些视图的绑定。您可以通过执行以下操作来实现此目的:

window.User = Backbone.Model.extend({
});

window.MyViewModel = Backbone.Model.extend({
});

window.myView = Backbone.View.extend({
    initialize: function(){
        this.model.on('change', this.alert, this); 
    },
    alert: function(){
        alert("changed"); 
    }
}); 

你需要将myView的模型设置为myViewModel,而myViewModel应该被设置为一个用户模型。这样,如果你将myViewModel设置为另一个用户(即更改其属性),那么它可以触发视图中的渲染函数并显示新的属性。

问题之一是这会破坏与原始模型的链接。你可以通过使用集合对象或将用户模型设置为视图模型的属性来解决这个问题。然后,在视图中可以通过myview.model.get("model")访问它。


1
污染全局范围从来不是一个好主意。为什么要在window命名空间上实例化BB.Models和BB.Views? - Vernon

0
使用此方法清除子视图和当前视图的内存。
//FIRST EXTEND THE BACKBONE VIEW....
//Extending the backbone view...
Backbone.View.prototype.destroy_view = function()
{ 
   //for doing something before closing.....
   if (this.beforeClose) {
       this.beforeClose();
   }
   //For destroying the related child views...
   if (this.destroyChild)
   {
       this.destroyChild();
   }
   this.undelegateEvents();
   $(this.el).removeData().unbind(); 
  //Remove view from DOM
  this.remove();  
  Backbone.View.prototype.remove.call(this);
 }



//Function for destroying the child views...
Backbone.View.prototype.destroyChild  = function(){
   console.info("Closing the child views...");
   //Remember to push the child views of a parent view using this.childViews
   if(this.childViews){
      var len = this.childViews.length;
      for(var i=0; i<len; i++){
         this.childViews[i].destroy_view();
      }
   }//End of if statement
} //End of destroyChild function


//Now extending the Router ..
var Test_Routers = Backbone.Router.extend({

   //Always call this function before calling a route call function...
   closePreviousViews: function() {
       console.log("Closing the pervious in memory views...");
       if (this.currentView)
           this.currentView.destroy_view();
   },

   routes:{
       "test"    :  "testRoute"
   },

   testRoute: function(){
       //Always call this method before calling the route..
       this.closePreviousViews();
       .....
   }


   //Now calling the views...
   $(document).ready(function(e) {
      var Router = new Test_Routers();
      Backbone.history.start({root: "/"}); 
   });


  //Now showing how to push child views in parent views and setting of current views...
  var Test_View = Backbone.View.extend({
       initialize:function(){
          //Now setting the current view..
          Router.currentView = this;
         //If your views contains child views then first initialize...
         this.childViews = [];
         //Now push any child views you create in this parent view. 
         //It will automatically get deleted
         //this.childViews.push(childView);
       }
  });

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