Skip to content
This repository

Fix #1339 - Add Backbone.View#destroy. #1353

Merged
merged 2 commits into from almost 2 years ago
brad dunbar
Collaborator

As stated in #1339, destroy is a fairly strong convention as this point. Should Backbone reinforce this convention or leave it to the user? If so, I'm of the opinion that it should be left blank so that the user can fill it in. Even the most minimal default implementation would have drawbacks and make assumptions about the structure of the user's code.

brad dunbar
Collaborator

If any implementation is to be provided, I think it should be very minimal. For instance:

if (this.model) this.model.off(null, null, this);
if (this.collection) this.collection.off(null, null, this);
Mathias Schäfer
molily commented May 28, 2012

I’m a strong supporter of adding these things to Backbone, at least as a convention. I have to admit I’m biased since I released a Backbone-based library which has a focus on object disposal and memory management. ;)

In Chaplin, we’re using the name dispose on all core classes since destroy is already taken on Model (where it has a totally different meaning). I’d love to see conventions for disposing models and collections in Backbone as well, but I guess you consider this to be out of scope. (Well, I thought this for View as well, good to see some progress here.)

As an overview, Thorax is calling it freeze, Marionette is calling it close. While Thorax and Marionette are using an event unbinding abstraction, Chaplin is already using the model./collection.off(null, null, this) solution since Backbone.Events supported it.

How about paving the cowpaths here?

Tim Branyen
Collaborator

I've called it cleanup in LayoutManager since that's what you're actually doing. I'd recommend calling it that instead.

2 cents

Andrew Terris
aterris commented May 29, 2012

I too think that it makes sense to solidify this convention. I would lean towards a complete no-op to keep this as light as possible, but could see the case for the simple implementation posted above

Tim Griesser
Collaborator

+1 on adding it with a base implementation rather than a no-op as it's very minimal and would likely be the most common use case (unlike render, which varies widely)

Sam Breed
Collaborator

@braddunbar any documentation to go along with this? IMO these stubbed in methods are only worthwhile if there are clear usage guidelines in the docs..

brad dunbar
Collaborator

@wookiehangover Good point, fleshing out docs will probably be informative as to including or not including this. I'll do a draft.

Matt Bower

I just started using Backbone and came to a similar conclusion. I'm glad I'm not the only one. I went with teardown. I add the following to my project to shim on global functionality:

Backbone.View.prototype.remove = function() {
    this.teardown().$el.remove();
    return this;
};

Backbone.View.prototype.teardown = function() {
    return this;
};

Then I would just override teardown on each View that needed additional cleanup.

brad dunbar
Collaborator

I've added some initial documentation for the destroy stub, along with an example usage. I think that it's best left unimplemented for the time being since this.model.off(null, null, this) is still a fairly new convention.

I think that adding some convention for destroying views has been unanimously approved so I'm going to merge this as is. However, please continue to comment or open issues regarding naming, implementation, or documentation.

brad dunbar braddunbar merged commit 7054ca4 into from June 03, 2012
brad dunbar braddunbar closed this June 03, 2012
Onsi Fakhouri
onsi commented June 23, 2012

Along similar lines, I wrote Coccyx to provide teardown-able view hierarchies. The intent is to provide a way to plug up leaky view hierarchies: just registerSubview()s as you make them and call tearDown() on a root view to bring the entire hierarchy down.

Coccyx also monkey-patches .on and .off (I know, I know, awful... "but it works, and I wrote tests around it"). This allows all contextualized event bindings (not just bindings to view.model and view.collection) to get ripped out when view.tearDown() is called.

Matt Bower

I think this.undelegateEvents() should be added to the destroy stub. As of right now, Backbone Views don't clean themselves up internally (I understand why you're hesitant to bake in this.model.off()). Granted, it's the developer's responsibility to clean up after themselves, but I think that if you have a declarative why to add functionality automatically on init, the default teardown method should mirror that setup.

Tim Griesser
Collaborator

@webbower jQuery cleans up all events bound with delegateEvents() when $el.remove() is called, so it probably wouldn't be necessary to add that to the destroy stub.

brad dunbar
Collaborator

@tgriesser That's true, but not all views call remove, especially if they're not the only view attached to the element. I think undelegateEvents would be a good start to an implementation (if one is to be provided).

Jeremy Ashkenas
Owner

Whoops -- I didn't realize this got merged in. Can we roll it back out of master, and discuss further?

If we're going to standardize a "destroy" method for views, I think it might as well not be a no-op. Some views won't need it ... and for the views that do need it, it might as well work out of the box.

brad dunbar braddunbar referenced this pull request from a commit June 25, 2012
brad dunbar Revert #1353 for further discussion.
This reverts commit 7054ca4, reversing
changes made to 7828d6d.
d8477f4
brad dunbar
Collaborator

Sure thing. Reverted in d8477f4.

Raymond Julin

+1 for this as a non no-op, a default implementation doing undelegateEvents, model.off and collection.off would suit our needs quite well.

We've standardized on freeze and unfreeze in our code, and I must say I prefer the explicitness of this.freeze().remove() for cleaning up, especially as we use freeze to disable views without pulling them from the DOM so we can unfreeze them a little later.

brad dunbar braddunbar referenced this pull request June 27, 2012
Merged

Add View#dispose. #1461

Oleg Slobodskoi
kof commented April 02, 2013

Just wanted to create the same issue ... I am also using .destroy on views, collections and models. My destroys do the same thing supposed here earlier - unbinding all events the view/collection/model has listened to + .remove on views.

  1. It is a VERY logical consequence of .destroy call
  2. on views .destroy should call .remove after unbinding events.
  3. Most of backbone users don't even think about unbinding events.

I have started to use destroy a long time ago, after the application started to get slower and slower. I started to investigate why. The reason was mostly views, which have been removed from the dom, but still listened to the events from collections/models and has processed/rendered stuff in stealth mode.

So it is a real issue with real use cases. Everybody should use destroy and be warned to use it in the documentation.

+100 for .destroy on everything.

Oleg Slobodskoi
kof commented April 02, 2013

Think about socket.io which can be used in collection/models .. it will listen on events and views will react on them the whole time. Switching views by removing the previous one will create an endless amount of event handlers rendering stuff behind the scenes.

Oleg Slobodskoi
kof commented April 02, 2013

jQuery is unbinding events from dom elements being removed via jquery. Backbone should follow the similar logic.

Oleg Slobodskoi
kof commented April 02, 2013

Something like this + current Model#destroy logic for model.

Backbone.View.prototype.destroy =
Backbone.Collection.prototype.destroy = 
Backbone.Model.prototype.destroy = function() {
    if (this.collection) this.collection.off(null, null, this);
    if (this.model) this.model.off(null, null, this);

    // Remove element if in view.
    if (this.$el) this.$el.remove();

    // Unbind own events.
    this.off();
};
Adam Krebs
Collaborator

@kof I'd imagine the majority of the memory leaks you're seeing could be fixed by using listenTo and stopListening instead of on/off, and following a pattern where on will only be used for objects that live longer than the calling object for easy cleanup. For what it's worth, Backbone calls stopListening in View#remove to automatically unbind your events.

Oleg Slobodskoi
kof commented April 03, 2013

@akre54 thanks for the info about View#remove, I also completely forgot about listenTo, because I am still using 0.9.2 version.

I just read some issues discussing why listenTo was introduced ... but I still not liking it.
I don't know any usecase where a view should remain listening to events after it is destroyed ...
I don't like .listenTo vs. .on ....
I will implement my #destroy methods and not going to use listenTo
Also .listenTo is only for views, what if a model is listening to a collection and has been destroyed/remove ...?

Its not nicely looking and inconsistent.

Casey Foster
Collaborator

@kof listenTo is a method in Events, and is therefore extended onto Model, Collection, Router, View, and even Backbone itself along with all of the other event methods. It's a great way to listen to other objects while not having to keep track of those other objects yourself. Instead, when you're done listening, simply call stopListening().

Oleg Slobodskoi
kof commented April 03, 2013

ok right, listenTo seems to be really the most clear way ...

I have played with the idea to define more default namespaces like 'models, collections, views' etc. and unbind all events from them on remove/destroy, but it isn't very intuitiv if instantiating something new inside of a view f.e..

Also introducing an array where you need to put everything you listen to in order to stop listening on remove/destroy could be an option.

Oleg Slobodskoi
kof commented April 03, 2013

off topic ... but

  1. shouldn't Collection#remove call Model#stopListening, like View#remove?
  2. shouldn't both places also call this.off() to remove all listener on the own instance?
Misha Ponizil

Hey @kof,

  1. Model#stopListening would remove all of that model's listeners. The model still exists after Collection#remove, it's just no longer part of the collection.
  2. Again, removing a model from a collection does not mean anything has been discarded- the only cleanup needed is the "all" event, which is taken care of with Collection#_removeReference:

    https://github.com/documentcloud/backbone/blob/master/backbone.js#L654

    As for turning off external listeners in View#remove, I'm not sure that's entirely necessary or intuitively correct behavior. If you have other modules listening for events from that view, those modules should probably take care of cleaning up those listeners on their own- behaving otherwise would break down some separation of concerns.

Adam Krebs
Collaborator

@kof the general rule of thumb (to paraphrase @mehcode from another discussion) is if the lifetime of your target is shorter than this, use target.on(...), else use this.listenTo(target, ...).

Oleg Slobodskoi
kof commented April 03, 2013
  1. you are right, this should happen in Model#destroy
  2. we could use .off(null, null, this) - so only cleanup events used inside of the current instance

@akre54 @mponizil do you know any use cases, where after a view/model has been destroyed, somebody who was listening on it should still receive any events?
I think if it is really destroyed, not just removed from the DOM or from the collection there is no.

The problem is now - we have no "destroy".

So actually View#remove does too much now, If I just want to remove a view from the dom and insert it somewere else, I will need to add listener again. Same for $el.remove() of jquery.

It seems to me if we want to clearly separate "remove/detach" from complete "destroy" we need this "destroy" thing.

Oleg Slobodskoi
kof commented April 03, 2013

@mponizil also there should be 'remove' event on the view if other listeners should stop listening to the view on remove.

Oleg Slobodskoi
kof commented April 03, 2013

Also there is no real conflict with current Model#destroy implementation. Current Model#destroy just should accept an options.sync and only if its true, send the 'delete' event. Otherwise just do the cleanups.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 2 unique commits by 1 author.

May 28, 2012
brad dunbar Fix #1339 - Add Backbone.View#destroy. dad7e27
Jun 03, 2012
brad dunbar Documentation for View#destroy. 767e751
This page is out of date. Refresh to see the latest.

Showing 2 changed files with 28 additions and 0 deletions. Show diff stats Hide diff stats

  1. 7  backbone.js
  2. 21  index.html
7  backbone.js
@@ -1183,6 +1183,13 @@
1183 1183
       return this;
1184 1184
     },
1185 1185
 
  1186
+    // **destroy** should clean up any references created by this view,
  1187
+    // preventing memory leaks.  The convention is for **destroy** to always
  1188
+    // return `this`.
  1189
+    destroy: function() {
  1190
+      return this;
  1191
+    },
  1192
+
1186 1193
     // Remove this view from the DOM. Note that the view isn't present in the
1187 1194
     // DOM by default, so calling this method may be a no-op.
1188 1195
     remove: function() {
21  index.html
@@ -385,6 +385,7 @@
385 385
       <li>– <a href="#View-dollar">$ (jQuery or Zepto)</a></li>
386 386
       <li>– <a href="#View-render">render</a></li>
387 387
       <li>– <a href="#View-remove">remove</a></li>
  388
+      <li>– <a href="#View-destroy">destroy</a></li>
388 389
       <li>– <a href="#View-make">make</a></li>
389 390
       <li>– <a href="#View-delegateEvents">delegateEvents</a></li>
390 391
       <li>– <a href="#View-undelegateEvents">undelegateEvents</a></li>
@@ -2337,6 +2338,26 @@ <h2 id="View">Backbone.View</h2>
2337 2338
       <tt>view.$el.remove();</tt>
2338 2339
     </p>
2339 2340
 
  2341
+    <p id="View-destroy">
  2342
+      <b class="header">destroy</b><code>view.destroy()</code>
  2343
+      <br />
  2344
+      The default implementation of <b>destroy</b> is a no-op.  It should be
  2345
+      overridden in order to clean up any references created by a view,
  2346
+      either to itself or other objects, in order to prevent memory leaks.
  2347
+      By convention, <b>destroy</b> should <tt>return this</tt> for
  2348
+      chainability.
  2349
+    </p>
  2350
+
  2351
+<pre>
  2352
+var View = Backbone.View.extend({
  2353
+  destroy: function() {
  2354
+    if (this.model) this.model.off(null, null, this);
  2355
+    if (this.collection) this.collection.off(null, null, this);
  2356
+    return this;
  2357
+  }
  2358
+});
  2359
+</pre>
  2360
+
2340 2361
     <p id="View-make">
2341 2362
       <b class="header">make</b><code>view.make(tagName, [attributes], [content])</code>
2342 2363
       <br />
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.