You can implement a custom delegateEvents() and undelegateEvents() in a Backbone view.
The Backbone.View constructor calls delegateEvents automatically. I thought that undelegateEvents was called when you remove the view with Backbone.View.prototype.remove, but it is not true.
So, which is the best way to do this manually? I have overridden the remove() view method with this code:
Backbone.View.prototype.remove = function() {
var remove = Backbone.View.prototype.remove;
if (this.undelegateEvents) {
this.undelegateEvents();
}
return remove.apply(this, arguments);
};
It works, but I don't know if is the best option. How should I do this?
As mu is too short suggested, the real answer to the question
which is the best way to do this manually?
is don't. Events are bound to DOM elements, and if those elements go away so do the bindings. undelegateEvents is designed to be used in situations where you aren't removing the DOM element, but still want to take the event bindings off of it (eg. when you set a new element for the view).
Related
jQuery holds references to DOM nodes in its internal cache until I explicitly call $.remove(). If I use a framework such as React which removes DOM nodes on its own (using native DOM element APIs), how do I clean up jQuery's mem cache?
I'm designing a fairly large app using React. For those unfamiliar, React will tear down the DOM and rebuild as needed based on its own "shadow" DOM representation. The part works great with no memory leaks.
Flash forward, we decided to use a jQuery plugin. After React runs through its render loop and builds the DOM, we initialize the plugin which causes jQuery to hold a reference to the corresponding DOM nodes. Later, the user changes tabs on the page and React removes those DOM elements. Unfortunately, because React doesn't use jQuery's $.remove() method, jQuery maintains the reference to those DOM elements and the garbage collector never clears them.
Is there a way I can tell jQuery to flush its cache, or better yet, to not cache at all? I would love to still be able to leverage jQuery for its plugins and cross-browser goodness.
jQuery keeps track of the events and other kind of data via the internal API jQuery._data() however due to this method is internal, it has no official support.
The internal method have the following signature:
jQuery._data( DOMElement, data)
Thus, for example we are going to retrieve all event handlers attached to an Element (via jQuery):
var allEvents = jQuery._data( document, 'events');
This returns and Object containing the event type as key, and an array of event handlers as the value.
Now if you want to get all event handlers of a specific type, we can write as follow:
var clickHandlers = (jQuery._data(document, 'events') || {}).click;
This returns an Array of the "click" event handlers or undefined if the specified event is not bound to the Element.
And why I speak about this method? Because it allow us tracking down the event delegation and the event listeners attached directly, so that we can find out if an event handler is bound several times to the same Element, resulting in memory leaks.
But if you also want a similar functionality without jQuery, you can achieve it with the method getEventHandlers
Take a look at this useful articles:
getEventHandlers
getEventListeners - chrome
getEventListeners - firebug
Debugging
We are going to write a simple function that prints the event handlers and its namespace (if it was specified)
function writeEventHandlers (dom, event) {
jQuery._data(dom, 'events')[event].forEach(function (item) {
console.info(new Array(40).join("-"));
console.log("%cnamespace: " + item.namespace, "color:orangered");
console.log(item.handler.toString());
});
}
Using this function is quite easy:
writeEventHandlers(window, "resize");
I wrote some utilities that allow us keep tracking of the events bound to DOM Elements
Gist: Get all event handlers of an Element
And if you care about performance, you will find useful the following links:
Leaking Memory in Single Page Apps
Writing Fast, Memory-Efficient JavaScript
JavaScript Memory Profiling
I encourage anybody who reads this post, to pay attention to memory allocation in our code, I learn the performance problems ocurrs because of three important things:
Memory
Memory
And yes, Memory.
Events: good practices
It is a good idea create named functions in order to bind and unbind event handlers from DOM elements.
If you are creating DOM elements dynamically, and for example, adding handlers to some events, you could consider using event delegation instead of keep bounding event listeners directly to each element, that way, a parent of dynamically added elements will handle the event. Also if you are using jQuery, you can namespace the events ;)
//the worse!
$(".my-elements").click(function(){});
//not good, anonymous function can not be unbinded
$(".my-element").on("click", function(){});
//better, named function can be unbinded
$(".my-element").on("click", onClickHandler);
$(".my-element").off("click", onClickHandler);
//delegate! it is bound just one time to a parent element
$("#wrapper").on("click.nsFeature", ".my-elements", onClickMyElement);
//ensure the event handler is not bound several times
$("#wrapper")
.off(".nsFeature1 .nsFeature2") //unbind event handlers by namespace
.on("click.nsFeature1", ".show-popup", onShowPopup)
.on("click.nsFeature2", ".show-tooltip", onShowTooltip);
Circular references
Although circular references are not a problem anymore for those browsers that implement the Mark-and-sweep algorithm in their Garbage Collector, it is not a wise practice using that kind of objects if we are interchanging data, because is not possible (for now) serialize to JSON, but in future releases, it will be possible due to a new algorithm that handles that kind of objects. Let's see an example:
var o1 = {};
o2 = {};
o1.a = o2; // o1 references o2
o2.a = o1; // o2 references o1
//now we try to serialize to JSON
var json = JSON.stringify(o1);
//we get:"Uncaught TypeError: Converting circular structure to JSON"
Now let's try with this other example
var freeman = {
name: "Gordon Freeman",
friends: ["Barney Calhoun"]
};
var david = {
name: "David Rivera",
friends: ["John Carmack"]
};
//we create a circular reference
freeman.friends.push(david); //freeman references david
david.friends.push(freeman); //david references freeman
//now we try to serialize to JSON
var json = JSON.stringify(freeman);
//we get:"Uncaught TypeError: Converting circular structure to JSON"
PD: This article is about Cloning Objects in JavaScript. Also this gist contain demos about cloning objects with circular references: clone.js
Reusing objects
Let's follow some of the programming principles, DRY (Don't Repeat Yourself) and instead of creating new objects with similar functionality, we can abstract them in a fancy way. In this example I will going to reuse an event handler (again with events)
//the usual way
function onShowContainer(e) {
$("#container").show();
}
function onHideContainer(e) {
$("#container").hide();
}
$("#btn1").on("click.btn1", onShowContainer);
$("#btn2").on("click.btn2", onHideContainer);
//the good way, passing data to events
function onToggleContainer(e) {
$("#container").toggle(e.data.show);
}
$("#btn1").on("click.btn1", { show: true }, onToggleContainer);
$("#btn2").on("click.btn2", { show: false }, onToggleContainer);
And there are a lot of ways to improve our code, having an impact on performance, and preventing memory leaks. In this post I spoke mainly about events, but there are other ways that can produce memory leaks. I suggest read the articles posted before.
Happy reading and happy coding!
If your plugin exposes a method to programatically destroy one of its instances (i.e. $(element).plugin('destroy')), you should be calling that in the componentWillUnmount lifecycle of your component.
componentWillUnmount is called right before your component is unmounted from the DOM, it's the right place to clean up all external references / event listeners / dom elements your component might have created during its lifetime.
var MyComponent = React.createClass({
componentDidMount() {
$(React.findDOMNode(this.refs.jqueryPluginContainer)).plugin();
},
componentWillUnmount() {
$(React.findDOMNode(this.refs.jqueryPluginContainer)).plugin('destroy');
},
render() {
return <div ref="jqueryPluginContainer" />;
},
});
If your plugin doesn't expose a way to clean up after itself, this article lists a few ways in which you can try to dereference a poorly thought out plugin.
However, if you are creating DOM elements with jQuery from within your React component, then you are doing something seriously wrong: you should almost never need jQuery when working with React, since it already abstracts away all the pain points of working with the DOM.
I'd also be wary of using refs. There are only few use cases where refs are really needed, and those usually involve integration with third-party libraries that manipulate/read from the DOM.
If your component conditionally renders the element affected by your jQuery plugin, you can use callback refs to listen to its mount/unmount events.
The previous code would become:
var MyComponent = React.createClass({
handlePluginContainerLifecycle(component) {
if (component) {
// plugin container mounted
this.pluginContainerNode = React.findDOMNode(component);
$(this.pluginContainerNode).plugin();
} else {
// plugin container unmounted
$(this.pluginContainerNode).plugin('destroy');
}
},
render() {
return (
<div>
{Math.random() > 0.5 &&
// conditionally render the element
<div ref={this.handlePluginContainerLifecycle} />
}
</div>
);
},
});
How about do this when the user exits the tab:
for (x in window) {
delete x;
}
This is much better to do, though:
for (i in $) {
delete i;
}
I understand that when a view is removed through .remove(), .stopListening() is called on that view to remove any event listeners associated with that view in Backbone. From the Backbone docs:
remove view.remove()
Removes a view from the DOM, and calls stopListening to remove any bound events that the view has listenTo'd.
I have views that are appended to a container that only have events related to dom actions on themselves through Backbone's events hook.
var View = Backbone.View.extend({
events : {
'input keyup' : 'searchDropdown'
},
searchDropdown: function () {
$('dropdown').empty();
//Appends views based on search
}
});
My question is really whether or not I'm leaking any memory (significant or not) when calling $.empty() on a container that effectively removes the view(s) appended inside of it. And if I am, is there any good convention for accessing and calling .remove() on those views?
You don't need any special framework for this but it's a good idea to implement removal properly and not depend on the browser being smart enough to do this. Sometimes in a large app you will find you specifically need to override the remove method to do some special cleanup - for instance you are using a library in that view which has a destroy method.
A modern browser tends to have a GC which is smart enough for most cases but I still prefer not to rely on that. Recently I came on to a project in Backbone which had no concept of subviews and I reduced the leaking nodes by 50% by changing to remove from empty (in Chrome 43). It's very hard to have a large javascript app not leak memory, my advice is to monitor it early on: If a DOM Element is removed, are its listeners also removed from memory?
Watch out for things which leak a lot of memory - like images. I had some code on a project that did something like this:
var image = new Image();
image.onLoad(.. reference `image` ..)
image.src = ...
Basically a pre-loader. And because we weren't explicitly doing image = null the GC never kicked in because the callback was referencing the image variable. On an image heavy site we were leaking 1-2mb with every page transition which was crashing phones. Setting the variable to null in a remove override fixed this.
Calling remove on subviews is as easy as doing something like this:
remove: function() {
this.removeSubviews();
Backbone.View.prototype.remove.call(this);
},
removeSubviews: function() {
if (!_.isEmpty(this.subViews)) {
_.invoke(this.subViews, 'remove');
this.subViews = [];
}
}
You just need to add your subview instances to an array. For example when you create a subview you could have an option like parentView: this and add it to the array of the parent. I have done more intricate subview systems in the past but that would work fine. On initialize of the views you could do something like:
var parentView = this.options.parentView;
if (parentView) {
(parentView.subViews = parentView.subViews || []).push(this);
}
When I use Backbone's model.destroy(), it seems to automatically remove that view from the DOM.
Is there a way for me to use destroy() to send the DELETE request, but remove the view from the DOM myself?
Something like:
this.model.destroy({
wait: true,
success: function(){
$('#myElement').animate({
"height" : "0",
1000,
function(){$('#myElement').remove()}
});
}
});
You need to override _onCollectionRemove() in whichever Collection view contains the item views (documentation). This is the function which is called when your model is removed from the collection, and it's also what's destroying your view. Specifically how you choose to override it is up to you, but it might be easiest to override it with your animation function, maybe along the following lines...
_onCollectionRemove: function(model) {
var view = this.children.findByModel(model);
var that = this;
view.$('#myElement').animate({
"height" : "0",
1000,
function(){
that.removeChildView(view);
that.checkEmpty();
}
});
}
If you prefer to handle the removal of the view manually in your destroy callback, just override _onCollectionRemove() to contain an empty function and do whatever you'd like in the callback of your delete request. I'd recommend the approach I describe above rather than doing it in your destroy callback, though. Completely eliminating the function and then handling it's responsibilities elsewhere in your code interferes with Marionette's intended event flow. Simply overriding the function with a different UI effect preserves the natural flow.
EDIT: Another user's previous answer (now deleted due to downvoting) suggested that it might be wise to call destroy after the UI effect was completed. This is not a good approach for the reason OP pointed out - if something goes wrong with the destroy method, (for example, if the remote server goes down) it appears to the user as if the model was deleted (the UI effect had already completed) even though the server was unreachable and the model remains.
the mentioned onBeforeDestroy method does not work for me. It throws an error in backbone (remove method missing)
My solution has the same aproach and is working very well in itemView
remove: function(){
this.$el.animate({"height" : "0"},500, function(){
$(this).remove();
});
},
Instead of focusing in the model event, we can focus on the view life cycle. For that purpose, Marionette makes the onBeforeDestroy callback available on Marionette.View (which is extended by all Marionette views). In your ItemView you'd define the callback like this
onBeforeDestroy: function () {
$('#myElement').animate({ "height" : "0", 1000 });
}
They're is an important caveat here. Since $.animate is an asynchronous function, it is possible that the view may be removed before $.animate finishes the transition. So, we have to make a modification to our onBeforeDestroy.
onBeforeDestroy: function () {
var animationDelay = 1000ms;
this.remove = _.delay(_.bind(this.remove, this), animationDelay );
this.$el.animate({ "height" : "0", animationDelay });
}
Essentially, what we did here is set the View.remove() method to fire after the animation has run through, ensuring that when this.remove is called, it's called asynchronously, after the animation has run through. You could also do this with Promises, I suppose, but that requires a bit more overhead.
You need to use one of:
collection.remove model
collection.reset collection.model
Every of this methods will re-render your collection or composite view.
It is not Good practice to remove element from collection/composite view directly by using js or jQuery;
I want to know when a DOM element generated by Ractive is ready. In my case, I want to use jquery to attach an autocomplete function onto the element. Ideally it would go something like this:
Template:
{{#list}}
<input type="text" proxy-load="attach-typeahead">
{{/list}}
Javascript:
ractive.on("attach-typeahead", function(event){
$(event.node).typeahead(...);
})
But the event never fires even though I remeber seeing proxy-load mentioned somewhere in the documentation. What's the proper way to do what I'm trying to do? Thanks.
Codler's answer is spot on - transitions can be used to attach behaviour to nodes (and detach it, with outro).
As of the latest (0.3.8) version, there's another method, which behaves similarly but is slightly more streamlined for this purpose: decorators.
The documentation hasn't been written yet (my bad), but you can see a typeahead decorator here. A decorator is simply a function that gets called as soon as a node is added to the DOM, and which returns an object with a teardown() method that gets called as soon as the node is removed from the DOM.
You can make a decorator globally available like so:
Ractive.decorators.foo = function ( node ) {
// do some setup work with the node here...
return {
teardown: function () {
// do any necessary cleanup here
}
};
};
Or you can specify per-instance decorators, as in the fiddle.
Another decorator example here, this time a sortable list.
The proxy-events are mentioned here in the documentation of ractive. Your example doesn't work because the input element does not have a native load event.
All the ractive functions have a complete function callback that fires when the rendering has completed. Maybe you can use that.
You can use the intro attribute. It is a transition in ractive. When the DOM are created, intro will be called.
You can find more info here https://github.com/RactiveJS/Ractive/wiki/Transitions
Update:
Per my comment, my problem was that I had an extra model that I was passing into the view that I was not unbinding events on. When I saw the event handler being triggered I assumed the source was from this.model instead of this.extra_model, because I had forgotten that this.extra_model was being used for error validations as well.
The solution was to add the following:
MyView = Backbone.extend({
//...
//add method to override BaseView
cleanUp: function() {
this.extra_model.off(null, null, this);
BaseView.prototype.cleanUp.apply(this, arguments);
},
//...
});
Thanks for reviewing the problem, and sorry for the programmer error.
All:
I'm having a problem with stale/zombie events still being bound after I've cleaned up a view. The problem comes when I bind a custom event to a model. When I remove the view from the dom, I call 'this.model.off(null, null, this);' as suggested on various message boards, but although I can see the 'custom-handler' callback getting deleted in chrome debugger tools, I still notice the event handler for 'custom-handler' getting called more times than it should (one extra for every time I recreate the view after cleaning it up) when triggering the event. Could someone tell me if my clean up code is missing something? Thanks in advance!
BaseView = Backbone.extend({
//...
displayErrors:function(){},
cleanUp: function(){
if (this.model) this.model.off(null, null, this);
if (this.collection) this.collection.off(null, null, this);
if (!this.options.persistDataAfterViewCleanup) {
if (this.model) delete this.model;
if (this.collection) delete this.collection;
}
//_.each(this.subViews, function(view){view.cleanUp();}); not needed yet.
this.undelegateEvents();
$(this.el).removeData().unbind();
//Remove view from DOM
this.$el.html('');
this.remove();
}
});
MyView = BaseView.extend({
initialize: function(){
//called manually from model using trigger
this.model.on('custom-handler', this.displayErrors, this);
}
});
Assuming you are on the newest version of Backbone (0.9.10), you should use the new Backbone.Events.listenTo method to bind your events listeners. Using this method Backbone will keep a reference to the object, and clear all event bindings automatically upon view.remove():
this.listenTo(this.model, 'custom-handler', this.displayErrors);
All the things you do in your cleanUp method (delete, undelegateEvents, removeData, unbind, $el.html('')) look a lot like voodoo programming. None of those steps should be necessary at all.
Your zombie views are most likely due to some reference to the view being held by your own code, either directly or indirectly. A reference can be held by an event handler, a bound function, an exported closure, or any number of things. I suggest you try to analyze your code, and use Chrome Developer Tools' Heap profiler tool to try to find the retained objects and their referencers.
Check out my answer in this SO question, where I describe a simple method for finding memory leaks in specific code paths. While your problem is not directly about leaking memory, it's about leaking references, whose retained heap size should help you find what's holding onto them.