Javascript Backbone.View:交换两个子视图,它们共享父视图中的一个元素

Javascript Backbone.View:交换两个子视图,它们共享父视图中的一个元素,javascript,jquery,backbone.js,backbone-views,Javascript,Jquery,Backbone.js,Backbone Views,注意:我知道其他库(例如木偶)可以大大简化基于视图的问题。但是,让我们假设这不是一个选项 假设我们有一个给定“记录”(即模型)的父视图。该父视图有两个子视图,一个用于显示记录的属性,另一个用于编辑它们(假设在这种情况下就地编辑不合适)。到目前为止,每当我需要删除/显示子视图时,我都会在传出视图上调用remove,在传入视图上调用new,因此每次都会销毁/创建它们。这非常简单,易于编码/管理 然而,似乎有必要找出是否有任何可行的替代方法(似乎是默认的)删除/创建方法-特别是因为之前已经问过几次,但

注意:我知道其他库(例如木偶)可以大大简化基于视图的问题。但是,让我们假设这不是一个选项

假设我们有一个给定“记录”(即模型)的父视图。该父视图有两个子视图,一个用于显示记录的属性,另一个用于编辑它们(假设在这种情况下就地编辑不合适)。到目前为止,每当我需要删除/显示子视图时,我都会在传出视图上调用
remove
,在传入视图上调用
new
,因此每次都会销毁/创建它们。这非常简单,易于编码/管理

然而,似乎有必要找出是否有任何可行的替代方法(似乎是默认的)删除/创建方法-特别是因为之前已经问过几次,但从未完全回答过(例如)

因此,我一直在试图找出如何让两个子视图在父视图中共享一个元素,从而避免每次都必须
删除
新建
它们。当一个需要激活时,它被渲染,而另一个被“静音”(即不响应事件)。因此,它们会不断地交换,直到父视图被删除,然后它们一起被删除。

我的想法可以在这里找到:

注意:交换在
RecordView.editRecord
RecordView.onRecordCancel
中执行

虽然这看起来效果不错,但我有一些顾虑

(1) 即使“非活动”视图上的事件绑定被静默,两个视图设置为同一元素是否会有问题?只要只渲染“活动”视图,这似乎不是问题

(2) 当两个子视图调用了
remove
(即
Backbone.View.remove
)时,它调用
this.$el.remove
。现在,当删除第一个子视图时,这实际上会删除它们共享的DOM元素。因此,当在第二个子视图上调用
remove
时,没有要删除的DOM元素,我想知道这是否会使该子视图难以清理自己-特别是如果它自己创建了许多DOM元素,这些元素在呈现第一个子视图时被重写了。。。。或者如果涉及子视图这里似乎有一个关于内存泄漏的潜在问题。

抱歉,我知道这有点复杂。我正处于知识库的边界,所以我不完全理解这里涉及的所有潜在问题(因此这个问题)。但我确实希望有人处理过类似的问题,并能就所有这些提供知情的意见

无论如何,下面是完整的(简化示例)代码:

// parent view for displaying/editing a record. creates its own DOM element.
var RecordView = Backbone.View.extend({
    tagName : "div",
    className : "record",
    events : {
        "click button[name=edit]" : "editRecord",
        "click button[name=remove]" : "removeRecord",
    },
    initialize : function(settings){

        // create the two subviews. one for displaying the field(s) and
        // one for editing them. they both listen for our cleanup event
        // which causes them to remove themselves. the display view listens
        // for an event telling it to update its data.

        this.displayView = new RecordDisplayView(settings);
        this.displayView.listenTo(this,"cleanup",this.displayView.remove);
        this.displayView.listenTo(this,"onSetData",this.displayView.setData);

        this.editView = new RecordEditView(settings);
        this.editView.listenTo(this,"cleanup",this.editView.remove);

        // the editView will tell us when it's finished.

        this.listenTo(this.editView,"onRecordSave",this.onRecordSave);
        this.listenTo(this.editView,"onRecordCancel",this.onRecordCancel);

        this.setData(settings.data,false);
        this.isEditing = false;
        this.activeView = this.displayView;

        // we have two elements within our recordView, one for displaying the
        // the header of the record (i.e., info that doesn't change) and
        // one for displaying the subView. the subView element will be
        // bound to BOTH of our subviews.
        this.html = "<div class='header'></div><div class='sub'></div>";
    },
    render : function(){
        // for an explanation of why .empty() is called first, see: https://stackoverflow.com/questions/21031852/backbone-view-delegateevents-not-re-binding-events-to-subview
        this.$el.empty().html(this.html);
        this.$(".header").empty().html("<p>Record ID: "+this.data.id+"</p><p><button name='edit'>Edit</button><button name='remove'>Remove</button></p>");
        this.delegateEvents(); // allows for re-rendering
        this.renderSubView();        
        return this;
    },
    // the subviews SHARE the same element.
    renderSubView : function() {
        this.activeView.setElement(this.$(".sub")).render();
    },
    remove : function() {        
        this.stopListening(this.displayView);
        this.stopListening(this.editView);
        this.trigger("cleanup");
        this.displayView = null;
        this.editView = null;
        return Backbone.View.prototype.remove.call(this);
    },
    // notify will only be false upon construction call
    setData : function(data,notify) {
        this.data = data;
        if ( notify ) {
            this.trigger("onSetData",data);
        }
    },
    /* Triggered Events */
    editRecord : function(event) {
        if ( !this.isEditing ) {
            this.isEditing = true;
            this.activeView.silence(); // silence the old view (i.e., display)
            this.activeView = this.editView;
            this.renderSubView();
        }
        event.preventDefault();
    },
    removeRecord : function(event) {
        this.remove(); // triggers `remove` on both subviews
        event.preventDefault();
    },
    /* Triggered Events from editView */
    onRecordSave : function(data) {
        this.setData(data,true);
        this.onRecordCancel();
    },
    onRecordCancel : function() {
        this.isEditing = false;
        this.activeView.silence(); // silence the old view (i.e., edit)
        this.activeView = this.displayView;
        this.renderSubView();
    }    
});

// child view of RecordView. displays the attribute. takes over an existing DOM element.
var RecordDisplayView = Backbone.View.extend({
    events : {
        // if steps are not taken to silence this view, this event will trigger when
        // the user clicks 'cancel' on the editView!
        "click button[name=cancel]" : "onCancel"
    },
    initialize : function(settings){
        this.setData(settings.data);
    },
    setData : function(data) {
        this.data = data;
    },
    render : function(){        
        this.$el.empty().html("<p><strong>Field:</strong> "+this.data.field+"</p>");
        return this;
    },
    remove : function() {        
        this.trigger("cleanup");
        this.data = null;
        return Backbone.View.prototype.remove.call(this);
    },
    // the view is still attached to a particular element in the DOM, however we do not
    // want it to respond to any events (i.e., it's sharing an element but that element has
    // been rendered to by another view, so we want to make this view invisible for the time
    // being).
    silence : function() {
        this.undelegateEvents();
    },
    /* Triggered Events */
    onCancel : function() {
        alert("I'M SPYING ON YOU! USER PRESSED CANCEL BUTTON!");
    }
});

// subView of RecordView. displays a form for editing the record's attributes. takes over an existing DOM element.
var RecordEditView = Backbone.View.extend({
    events : {
        "click button[name=save]" : "saveRecord",
        "click button[name=cancel]" : "cancelRecord"
    },
    initialize : function(settings){
        this.data = settings.data;        
    },
    render : function(){
        this.html = "<form><textarea name='field' rows='10'>"+this.data.field+"</textarea><p><button name='save'>Save</button><button name='cancel'>Cancel</button></p></form>";
        this.$el.empty().html(this.html);
        return this;        
    },
    remove : function() {        
        this.trigger("cleanup");
        this.data = null;
        return Backbone.View.prototype.remove.call(this);
    },
    silence : function() {
        this.undelegateEvents();
    },
    /* Triggered Events */
    saveRecord : function(event){
        this.data.field = this.$("form textarea[name=field]").val();        
        this.trigger("onRecordSave",this.data);
        event.preventDefault();
    },
    cancelRecord : function(event){
        event.preventDefault();
        this.trigger("onRecordCancel");
    }    
});

// presumably this RecordView instance would be in a list of some sort, along with a bunch of other RecordViews.
var v1 = new RecordView({
    data : {id:10,field:"Hi there. I'm some text!"}
});
$("#content").empty().html(v1.render().$el);
//$("#content").empty().html(v1.render().$el); (re-rendering would work fine)
//用于显示/编辑记录的父视图。创建自己的DOM元素。
var RecordView=Backbone.View.extend({
标记名:“div”,
类名:“记录”,
活动:{
“单击按钮[名称=编辑]:“编辑记录”,
“单击按钮[名称=删除]:“删除记录”,
},
初始化:功能(设置){
//创建两个子视图。一个用于显示字段,另一个用于
//一个用于编辑它们。它们都侦听我们的清理事件
//这会导致它们自己删除。显示视图将侦听
//用于通知它更新其数据的事件。
this.displayView=新记录显示视图(设置);
this.displayView.listenTo(这个“清理”,这个.displayView.remove);
this.displayView.listenTo(this,“onSetData”,this.displayView.setData);
this.editView=新记录editView(设置);
this.editView.listenTo(this,“cleanup”,this.editView.remove);
//编辑视图将告诉我们何时完成。
this.listenTo(this.editView,“onRecordSave”,this.onRecordSave);
this.listenTo(this.editView,“onRecordCancel”,this.onRecordCancel);
this.setData(settings.data,false);
this.isEditing=false;
this.activeView=this.displayView;
//recordView中有两个元素,一个用于显示
//记录的标题(即不变的信息)和
//一个用于显示子视图。子视图元素将是
//绑定到我们的两个子视图。
this.html=“”;
},
render:function(){
//有关首先调用.empty()的原因的解释,请参阅:https://stackoverflow.com/questions/21031852/backbone-view-delegateevents-not-re-binding-events-to-subview
this.el.empty().html(this.html);
this.$(“.header”).empty().html(“记录ID:“+this.data.ID+”

EditRemove

”; this.delegateEvents();//允许重新呈现 这个.renderSubView(); 归还这个; }, //子视图共享相同的元素。 renderSubView:函数(){ this.activeView.setElement(this.$(“.sub”)).render(); }, 删除:函数(){ this.stopListening(this.displayView); this.stopListening(this.editView); 本条。触发(“清理”); this.displayView=null; this.editView=null; 返回Backbone.View.prototype.remove.call(this); }, //通知仅在施工呼叫时为假 setData:功能(数据、通知){ 这个数据=数据; 如果(通知){ 此触发器(“onSetData”,数据); } }, /*触发事件*/ editRecord:函数(事件){ 如果(!this.isEditing){ this.isEditing=true; this.activeView.silence();//使旧视图静音(即显示) this.activeView=this.editView; 这个.renderSubView(); } event.preventDefault(); }, 去除剂
formViewRecord: {},     
viewManagement: function(view) {

  var that = this, clone;

    this.formViewRecord[view.cid] = view;
clone = _.omit(_.clone(that.formViewRecord), view.cid);  // take out the current view
    _.each(clone, function(element, index, array) {
        var e = element.el
            , dupNode = e.cloneNode(false)  // shallow clone only
            , parentDiv = e.parentNode
            ;
        parentDiv.insertBefore(dupNode, e);  // need to insert a shallow copy of the dom node
        element.remove();  // remove() literally removes the view. 
        delete that.formViewRecord[element.cid];                
    });
},
this.listenTo(this.dispatcher, 'viewManagement', this.viewManagement, this);
// this.dispatcher = _.extend({}, Backbone.Events);
initialize: function(options) {

  var that = this;
      this.dispatcher = options.dispatcher;
      this.dispatcher.trigger('viewManagement', this);
<div class="item is-editing">
  <div class="edit-view">...</div>
  <div class="read-view">...</div>
</div>
// equivalent of creating a parent view with a subview through backbone, assuming both
// creating new DOM elements
var parentView = $("<div></div>").attr("id","parent").html("<div class='sub'></div>");          

// equivalent to assigning two subviews to the same element in a parent view. no
// problems here.
var subView1 = parentView.find(".sub");
var subView2 = parentView.find(".sub");

// they both reference the same element (outside DOM still), so both would have data
// of 'idx' = 2. there are no problems with this.
subView1.data("idx",1);
subView2.data("idx",2);

// add parentView to the DOM, which adds the element that subView1 and 2 reference.
$("#content").append(parentView);           

// equivalent to rendering one subview in backbone and using setElement to swap.
// again, no problems with any of this. you can see that the setElement calls
// happening again and again would be redundant.

subView1 = parentView.find(".sub");
var activeSubView = subView1;           
activeSubView.html("subView1: " + subView1.data("idx")); // subView1: 2

subView2 = parentView.find(".sub");
activeSubView = subView2;
activeSubView.html("subView2: " + subView2.data("idx")); // subView2: 2

// when you `remove`, all it does is remove the element from the DOM and empty out
// its jQuery data ("idx") and unbind all the events. nothing is "destroyed". you
// still have a reference to it, so it won't be gc'd. the only difference between
// `remove` and `detach` is that `detach` keeps the jQuery data and events. there
// is no need to `remove` the subViews explicitly, as they are children of the
// parent and so when the parent is removed from the DOM, they come with it.

//subView1.remove();
//subView2.remove();
parentView.remove();

// all of the HTML inside the parentView and subView elements still exists. their events are
// gone and their jQuery data is gone.

console.log(subView1.html()); // "subView2: 2"
console.log(parentView.html()); // "<div class="sub">subView2: 2</div>"

console.log(subView1.data("idx")); // undefined
console.log(subView2.data("idx")); // undefined

// this will ensure that they are cleaned up.
parentView = subView1 = subView2 = null;