【问题标题】:Backbone.js : repopulate or recreate the view?Backbone.js:重新填充或重新创建视图?
【发布时间】:2011-11-25 21:56:08
【问题描述】:

在我的 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;
    }
})

【问题讨论】:

    标签: javascript templates backbone.js


    【解决方案1】:

    我总是销毁和创建视图,因为随着我的单页应用程序变得越来越大,将未使用的实时视图保留在内存中以便我可以重复使用它们将变得难以维护。

    这是我用来清理视图以避免内存泄漏的技术的简化版本。

    我首先创建了一个 BaseView,我的所有视图都从该 BaseView 继承。基本思想是,我的 View 将保留对其订阅的所有事件的引用,以便在处理 View 时,所有这些绑定都将自动解除绑定。这是我的 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 开始,listenTo 和 stopListening 使用上面显示的相同 bindTo 和 unbindFromAll 技术添加到事件中。另外,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 中删除元素。我应该一直将视图保留在内存中吗?
    • 嗨fisherwebdev。您也可以将此技术与 Backbone.View.extend 一起使用,但您需要在 BaseView.initialize 方法中初始化 this.bindings。这样做的问题是,如果您继承的视图实现了自己的初始化方法,那么它将需要显式调用 BaseView 的初始化方法。我在这里更详细地解释了这个问题:stackoverflow.com/a/7736030/188740
    • 嗨 SunnyRed,我更新了我的答案以更好地反映我破坏视图的原因。使用 Backbone,我认为没有理由在应用程序启动后重新加载页面,因此我的单页应用程序变得非常大。当用户与我的应用程序交互时,我会不断地重新渲染页面的不同部分(例如从详细视图切换到编辑视图),因此我发现始终创建新视图要容易得多,无论该部分之前是渲染还是不是。另一方面,模型代表业务对象,所以我只会在对象真正改变时修改它们。
    【解决方案2】:

    我最近写了一篇博客,并展示了我在我的应用中为处理这些场景所做的几件事:

    http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/

    【讨论】:

    • 为什么不在路由器中只使用delete view
    • 我赞成您的答案,但如果将博客文章的相关部分包含在答案本身中,这将真正受益,因为这是这里的目标。
    【解决方案3】:

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

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

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

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

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

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

    当然,这样做需要item View 引用的通用模型以及更高级别的列表视图和详细信息视图。

    希望这会有所帮助!

    【讨论】:

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

    要修复多次绑定事件,

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

    在从路由实例化新视图之前使用上述行,解决了我遇到的僵尸视图问题。

    【讨论】:

    • 这里有很多非常好的、详细的答案。我绝对打算研究 ViewManger 的一些建议。然而,这个非常简单,它对我来说非常有效,因为我的视图都是带有 close() 方法的面板,我可以在其中取消绑定事件。谢谢阿山
    • 我解除绑定后似乎无法重新渲染:\
    • @FlyingAtom:即使我在解除绑定后也无法重新渲染视图。你有没有办法做到这一点?
    • view.$el.removeData().unbind();
    【解决方案5】:

    我认为大多数人从 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 的示例,但在这种情况下,这似乎更灵活。我的问题是,为什么没有更多的人使用这种技术?
    • 也许是因为他是 Backbone 方面的专家 :)。我认为这种技术很简单,使用起来也很安全,我一直在使用它,到目前为止没有问题:)
    【解决方案6】:

    另一种方法是绑定,而不是创建一系列新视图然后解除绑定这些视图。您可以通过以下方式完成此操作:

    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 设置为另一个用户(即更改其属性),那么它可能会在具有新属性的视图中触发渲染函数。

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

    【讨论】:

    • 污染全局范围绝不是一个好主意。为什么要在窗口命名空间上实例化 BB.Models 和 BB.Views?
    【解决方案7】:

    使用此方法从内存中清除子视图和当前视图。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多