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;
    }
})

Answers:



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;

每当View需要绑定到模型或集合上的事件时,我都会使用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添加到了Events中。另外,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:gist.github.com/1288947,但我想可以做得更好。
德米特里·普洛什金

德米特里(Dmitry),我所做的事情与您处理嵌套视图的工作类似。我尚未看到更好的解决方案,但我也想知道是否有解决方案。这也是与此相关的另一个讨论:groups.google.com/forum/# ! topic/backbonejs/3ZFm-lteN-A。我注意到,在您的解决方案中,您没有考虑直接处理嵌套视图的情况。在这种情况下,即使已放置嵌套视图,父视图仍将保留对嵌套视图的引用。我不知道您是否需要考虑这一点。
约翰尼·奥什卡

如果我具有可打开和关闭同一视图的功能该怎么办。我有一个前进和后退的按钮。如果我打电话给dispose,它将从DOM中删除该元素。我应该一直将视图保存在内存中吗?
dagda1 2011年

1
嗨fisherwebdev。您还可以将这种技术与Backbone.View.extend一起使用,但是您需要在BaseView.initialize方法中初始化this.bindings。这样做的问题是,如果继承的视图实现了自己的initialize方法,则它将需要显式调用BaseView的initialize方法。我在这里更详细地说明了这个问题:stackoverflow.com/a/7736030/188740
Johnny Oshika,2012年

2
嗨,SunnyRed,我更新了答案,以更好地反映出破坏视图的原因。使用Backbone,我认为没有理由在应用启动后重新加载页面,因此我的单页面应用变得非常大。当用户与我的应用进行交互时,我会不断地重新渲染页面的不同部分(例如,从详细信息切换到编辑视图),因此,无论以前是否渲染该部分,始终创建新视图都变得更加容易。不。另一方面,模型代表业务对象,因此,仅当对象确实发生更改时,我才对其进行修改。
约翰尼·奥希卡

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 2011年

6

要修复多次绑定的事件,

$("#my_app_container").unbind()
//Instantiate your views here

在从路线实例化新的Views之前使用上面的代码,解决了我在僵尸视图中遇到的问题。


这里有很多很好的详细答案。我绝对打算研究一些ViewManger建议。但是,这一操作非常简单,对我来说效果很好,因为我的视图都是具有close()方法的面板,在这里我可以取消绑定事件。感谢Ashan
netpoetica 2013年

2
我解除绑定后似乎无法重新渲染:\
CodeGuru

@FlyingAtom:即使解除绑定后,我也无法重新渲染视图。您找到任何办法吗?
Raeesaa 2014年

view。$ el.removeData()。unbind();
亚历山大·米尔斯

2

我认为大多数以Backbone开头的人都会按照您的代码创建视图:

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

这段代码创建了僵尸视图,因为我们可能会不断创建新视图而不清理现有视图。但是,为应用程序中的所有主干视图调用view.dispose()并不方便(特别是如果我们在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];
}

使用VM创建视图将有助于清理任何现有视图,而无需调用view.dispose()。您可以从

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

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


我最近与骨干网进行了广泛的合作,这似乎是在构建或重用视图时处理僵尸视图的最充实的方法。我通常遵循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,它将被设置为User模型。这样,如果将myViewModel设置为另一个用户(即更改其属性),则它可以使用新属性触发视图中的渲染功能。

一个问题是,这断开了与原始模型的链接。您可以通过使用收集对象或将用户模型设置为viewmodel的属性来解决此问题。然后,可以在视图中将其作为myview.model.get(“ model”)进行访问。


1
污染全球范围从来都不是一个好主意。为什么要在窗口名称空间上实例化BB.Models和BB.Views?
弗农

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);
       }
  });
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.