Impossible Backbone Zombies - javascript

I've been trying to debug my Backbone multi-page app for most of the day now to get rid of 'zombies', but unfortunately to no avail. Before today, I didn't even realize I have a zombie problem. What am I doing wrong?
This is my RegionManager:
var regionManager = (function() {
var currView = null;
var rm = {};
var closeView = function(view) {
if (view && view.close) {
view.close();
}
};
var openView = function(view) {
view.render();
if (view.onShow) {
view.onShow();
}
};
rm.show = function(view) {
closeView(currView);
currView = view;
openView(currView);
};
return rm;
})();
This is my View cleaning up function:
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
if (this.views) {
_.invoke(this.views, 'close');
}
// Unbind any view's events.
this.off();
// Unbind any model and collection events that the view is bound to.
if (this.model) {
this.model.off(null, null, this);
}
if (this.collection) {
this.collection.off(null, null, this);
}
// Clean up the HTML.
this.$el.empty();
};
I tried appending the View els directly to the body and using this.remove(); in the View clean-up function (instead of using a common el: $('#content') to which I am appending elements, then cleaning up by this.$el.empty()), but that didn't work either.
It might have something to do with my "global Events":
Backbone.Events.on('letterMouseDown', this.letterMouseDown, this);
But I take care of them with the onClose function:
onClose: function() {
Backbone.Events.off('letterMouseDown');
}

One problem I see is that your close function never removes the event delegator from the view's el. A view's events are handled by using the delegator form of jQuery's on to attach a single event handler to the view's el. Your close does:
this.$el.empty();
but that only removes the content and any event handlers attached to that content, it does nothing at all to the handlers attached directly to this.el. Consider this minimal example:
var V = Backbone.View.extend({
events: {
'click': 'clicked'
},
clicked: function() {
console.log('still here');
}
});
var v = new V({ el: '#el' });
v.close();
After that, clicking on #el will throw a 'still here' in the console even though you think that the view has been fully cleaned up. Demo: http://jsfiddle.net/ambiguous/aqdq7pwm/
Adding an undelegateEvents call to your close should take care of this problem.
General advice:
Don't use the old-school on and off functions for events, use listenTo and stopListening instead. listenTo keeps track of the events on the listener so it is easier to remove them all later.
Simplify your close to just this:
Backbone.View.prototype.close = function() {
if(this.onClose)
this.onClose();
if(this.views)
_.invoke(this.views, 'close');
this.remove();
};
Don't bind views to existing els. Let the view create (and own) its own el and let the caller place that el into a container with the usual:
var v = new View();
container.append(v.render().el);
pattern. If you must attach to an existing el then the view should override remove with a slightly modified version of the standard implementation:
remove: function() {
this.$el.empty(); // Instead of removing the element.
this.undelegateEvents(); // Manually detach the event delegator.
this.stopListening();
return this;
}

I'm pretty sure I found the root for my problem.
mu is too short was right, with the close() method I wasn't removing the events bound directly to my el (which I tried to do by this.off() - this.$el.off()/this.undelegateEvents() is the correct way). But for me, it only fixed the problem that events got called multiple times unnecessarily.
The reason I was plagued by 'zombie views' or unintended behavior was that I wasn't freeing up the memory in the View..
this.remove() only gets rid of the el and it's elements/events, but not the View's internal variables. To elaborate - in my View I have an array declared like so this.array: [] and I didn't have it freed in the onClose function.
All I had to do was empty it in the onClose function or initially declare the array as this.array: null so on recurrent View renderings it would at least free the previous array (it still should be freed on the onClose method though, because the array/object is still going to sit in the memory until browsing away from the page).
It was excruciating to debug, because it's a crossword game (at least my code is hard to read there) and sometimes the words didn't match up, but I didn't know where the problem was coming from.
Lessons learned.

Related

Fundamental Backbone issue with registering event handlers

I really don't understand how Backbone is supposed to handle common scenarios where you want to register event handlers on HTML before the HTML is inserted into the DOM.
Most Backbone views look like so:
var PortalView = Backbone.View.extend({
events: {
'click #logout-li-id': 'onClickLogout' //1 (doesn't work)
},
initialize: function (opts) {
var self = this;
$('#logout-li-id').on('click', function (event) { //2 (doesn't work because '#logout-li-id' is not in the DOM yet
event.preventDefault();
alert(event);
});
},
render: function () {
var self = this;
var template = allTemplates['templates/portalTemplate.ejs'];
var ret = EJS.render(template, {});
$('#main-div-id').html(ret);
$('#logout-li-id').on('click', function (event) { //3 works!
event.preventDefault();
alert(event);
});
return this;
},
onClickLogout: function(event){
alert(event);
}
});
As you can see above, I have labeled the 3 similar calls to register an event handler on the DOM element '#logout-li-id' - the problem is that the only successful call to register the callback on the DOM element is in the render function (call #3), which occurs after the el for this Backbone view is inserted in the DOM. So, the standard way of declaring a key in the events object for the Backbone view doesn't work! So what is the right way to do this with Backbone?
Backbone expects that events a view will handle via the events hash are children of the view's el (that is, the root DOM element of the view). It looks like you are probably not attaching this view to the parent of the DOM element you're listening to - is $('#main-div-id') a child of the view's el?
The standard approach here would be something like:
var PortalView = Backbone.View.extend({
// Associate this view with the element it will manage
el: '#main-div-id',
events: {
'click #logout-li-id': 'onClickLogout'
},
render: function () {
var template = allTemplates['templates/portalTemplate.ejs'];
var ret = EJS.render(template, {});
// Render to this view's element
this.$el.html(ret);
}
});
More detail: The events hash works by adding delegated event handlers to the view's DOM node. Essentially, every click within the view's DOM tree will call a handler that checks whether the event target matches a given selector. If so, it passes the event to the view method you specify. But this only works if the element you click is in the view's DOM tree - otherwise, the view's DOM will never get the event and the event handler will never be called.

Where would you place Backbone View related code that needs to be run after insertion in the page

From what I understand of the way Backbone.js is intended to be used, Views are supposed to be rendered in their own $el element, which is not necessarily attached to the page. If it is so, the higher level view they depend on usually takes care of inserting the $el in the page.
I am making this statement after having read the Todo sample application. In this case, the TodoView renders element in a default div element that is not attached to the page.
var TodoView = Backbone.View.extend({
// [...]
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$el.toggleClass('done', this.model.get('done'));
this.input = this.$('.edit');
return this;
},
The AppView, upon a Todo creation, takes care of creating the TodoView and appending its $el to the page after rendering.
var AppView = Backbone.View.extend({
// [...]
addOne: function(todo) {
var view = new TodoView({model: todo});
this.$("#todo-list").append(view.render().$el);
},
My question is: If a view not attached to the page needs adjustments after being inserted (e.g. calculating its position in the viewport and performing DOM manipulation accordingly), where would you place the corresponding code?
Would you create a afterInsertion() method that the sublevel View should call after inserting, would you put the code at the same emplacement that where the insertion takes place (i.e. in the sublevel View) or would you change the way the view works to have it rendering directly in the page? I'm sure there are other solutions I can't think of right now. I would like to know what you consider being a best practice/optimized in the way Backbone should work, or if this question doesn't make sense to explain why.
I keep track of my sub-views. In a base view definition, create an add method:
var BaseView = Backbone.View.extend({
// It is really simplified... add the details you find necessary
add: function (name, viewDef, options) {
options = options || {};
if (!this.children)
this.children = {};
var view = new viewDef(options);
this.children[name] = view;
this.listenToOnce(view, 'ready', this.onSubViewReady);
view.render(options);
view.$el.appendTo(options.appendTo || this.$el);
}
});
With this, you can keep track of your subviews and make anything you want with them later.
If you feel like making things "automatic", you can trigger a ready event after your render method doing this:
var extend = BaseView.extend;
BaseView.extend = function (protoProps, staticProps) {
var child = extend.apply(this, arguments);
child.prototype.__render__ = protoProps['render'] || this.prototype.__render__ || function() {};
child.prototype.render = function () {
this.__render__.apply(this, arguments);
this.trigger('ready', this);
}
};
With this you can do a lot already.
Just remember that the DOM won't be drawn by the time that ready is triggered. So, if you are willling to do any calculations with the subview height or anything that needs the DOM to be drawn, use setTimeout(function () { ... }, 0) to put your code to the end of queue.
Hope I've helped.

Simple event aggregator

I'm looking for a simple event aggregator that works with require.js. I have two modules, one containing a view model and another with a "listener" of some sort:
// view model
define(['lib/knockout-2.2.1', 'events/aggregator'], function(ko, events){
var squareViewModel = function(contents) {
this.click = function(){
events.publish('squareClicked', this);
};
};
return squareViewModel;
});
// some listener of some kind
define(['events/aggregator'], function(events){
events.subscribe('squareClicked', function(e){
alert("hurrah for events");
});
});
Is there anything out there that does this? Is this kind of architecture even a good idea? This is my first foray into client-side architecture.
This is similar to what you posted, but I've had good luck with extending Backbone events (you don't actually have to use anything else about Backbone for this to work), something like this:
define(['underscore', 'backbone'], function( _, Backbone ) {
var _events = _.extend({}, Backbone.Events);
return _events;
});
And then all your viewmodels (or any code really) can use it:
define(['knockout', 'myeventbus'], function(ko, eventBus){
return function viewModel() {
eventBus.on('someeventname', function(newValue) {
// do something
});
this.someKOevent = function() {
eventBus.trigger('someotherevent', 'some data');
};
};
});
The original idea came from this article by Derick Bailey. One of my other favorites on client-side events is this one by Jim Cowart
Again, it amounts to nearly the same thing as what you posted. However, the one thing I don't like about the approach of tying it to jQuery document DOM node is that you might have other types of events flying through there as well, bubbled up from child nodes, etc. Whereas by extended backbone events you can have your own dedicated event bus (or even more than one if you e.g. wanted to have separate data events vs. UI events).
Note regarding RequireJS in this example: Backbone and Underscore are not AMD-compatible, so you'll need to load them using a shim config.
I ended up using jQuery to make my own:
define([], function(){
return {
publish: function (type, params){
$(document.body).trigger(type, params);
},
subscribe: function(type, data, callback){
$(document.body).bind(type, data, callback);
},
};
});
It works for what I want it for, but it has not been extensively tested.
As explunit points out in his answer, this will capture any events on document.body. I also discovered a scoping issue when accessing this inside of a passed callback function.

Backbone.JS Custom Event from Child View

I have two views, for simplicity sake a parent/child. The child is using trigger to throw an event. I am not seeing it in the handler of the parent. Is the below valid?
var parent = Backbone.View.extend({
events: { "childevent": "run" },
run: function(e) {
console.log(e);
},
render: function() { /* render the child here */ }
});
var child = Backbone.View.extend({
someAction: function() { this.trigger('childevent'); }
});
Figured it out! $(this.el).trigger('childevent'); works.
Shouldn't it be events: { "childevent": "run" } instead? There is no way to access the actual anonymous function in this place in the code.
Backbone store a jQuery reference to the view's node in this.$el property so you can spare some performance using it rather than re-compute the reference by $(this.el).
// use "this.$el.trigger()" to refer to jQuery's object
this.$el.trigger('childevent');
Obviously late, but for anyone else who comes across this:
the events property on a view is for auto-binding Html DOM events from components within the views el or $el elements, and the syntax involves both a UI event and a selector as the key in pair:
events: { "click #someButton", "clickHandler" }
To listen to events from other Models or Views, you use this.listenTo( target, ..... )

JS: using 'var me = this' to reference an object instead of using a global array

The example below, is just an example, I know that I don't need an object to show an alert box when user clicks on div blocks, but it's just a simple example to explain a situation that frequently happens when writing JS code.
In the example below I use a globally visible array of objects to keep a reference to each new created HelloObject, in this way events called when clicking on a div block can use the reference in the arry to call the HelloObject's public function hello().
1st have a look at the code:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
<title>Test </title>
<script type="text/javascript">
/*****************************************************
Just a cross browser append event function, don't need to understand this one to answer my question
*****************************************************/
function AppendEvent(html_element, event_name, event_function) {if(html_element) {if(html_element.attachEvent) html_element.attachEvent("on" + event_name, event_function); else if(html_element.addEventListener) html_element.addEventListener(event_name, event_function, false); }}
/******************************************************
Just a test object
******************************************************/
var helloobjs = [];
var HelloObject = function HelloObject(div_container)
{
//Adding this object in helloobjs array
var id = helloobjs.length; helloobjs[id] = this;
//Appending click event to show the hello window
AppendEvent(div_container, 'click', function()
{
helloobjs[id].hello(); //THIS WORKS!
});
/***************************************************/
this.hello = function() { alert('hello'); }
}
</script>
</head><body>
<div id="one">click me</div>
<div id="two">click me</div>
<script type="text/javascript">
var t = new HelloObject(document.getElementById('one'));
var t = new HelloObject(document.getElementById('two'));
</script>
</body></html>
In order to achive the same result I could simply replace the code
//Appending click event to show the hello window
AppendEvent(div_container, 'click', function()
{
helloobjs[id].hello(); //THIS WORKS!
});
with this code:
//Appending click event to show the hello window
var me = this;
AppendEvent(div_container, 'click', function()
{
me.hello(); //THIS WORKS TOO AND THE GLOBAL helloobjs ARRAY BECOMES SUPEFLOUS!
});
thus would make the helloobjs array superflous.
My question is: does this 2nd option in your opinion create memoy leaks on IE or strange cicular references that might lead to browsers going slow or to break???
I don't know how to explain, but coming from a background as a C/C++ coder, doing in this 2nd way sounds like a some sort of circular reference that might break memory at some point.
I also read on internet about the IE closures memory leak issue http://jibbering.com/faq/faq_notes/closures.html (I don't know if it was fixed in IE7 and if yes, I hope it does not come out again in IE8).
Thanks
Aside:
var HelloObject = function HelloObject(div_container)
In general try not to use named inline function expressions. It doesn't usually get you anything and there are serious problems with them in JScript (IE). Either use a function HelloObject() { ... } statement, or a var HelloObject= function() { ... }; anonymous expression.
does this 2nd option in your opinion create memoy leaks on IE
‘Create’? No, your existing code already had leaks on IE.
The listener you applied to the click event has a closure, keeping a reference to the parent function scope. In both examples, the div_container object is in that scope, so you've got a circular reference:
div -> onclick ->
listener function -> parent scope ->
parent function -> reference to div
A reference loop containing a mixture of native JS objects and host objects (such as the div, a DOM Node) is what causes the memory leaks in IE6-7.
To stop this happening, you can pull the click function definition out of the parent:
function HelloObject(div_container) {
AppendEvent(div_container, 'click', HelloObject_hello);
}
function HelloObject_hello() {
alert('hello');
}
Now there is no closure, the div_container is not remembered, and there is no loop/leak. However, the hello function is just a function, with no idea to which div_container it belongs (other than by looking at the event/this it gets on click).
More typically you do need to remember the div, or, if you're doing things in an objecty way, the this:
function HelloObject(element) {
this.element= element;
AppendEvent(element, 'click', this.hello.bind(this));
}
HelloObject.prototype.hello= function() {
alert('Hello, you clicked on a '+this.element);
};
(About function.bind.)
Which of course brings back the reference loop:
element -> onclick
bound hello function -> bound this
Helloer instance -> 'element' member
reference to element
Do you really care about this kind of refloop? Maybe not. It only really affects IE6-7; it's bad in IE6 as the memory isn't given back until you quit the browser, but then there's a growing school of thought that says anyone still using IE6 deserves everything they get.
On IE7, the memory leaks pile up until you leave the page, so it only matters for very long-running pages where you're throwing away old HelloObjects and binding new ones repeatedly. (In that case, a linear Array of HelloObjects that doesn't discard old values would also be a memory leak until page unload, in itself.)
If you do care, because you're working for some dingy corporate that runs IE6 and no-one ever closes their browsers, then (a) my condolences, and (b) yes, you will indeed have to implement something like the lookup object you had, to act as a decoupling layer between the DOM Nodes and the Function object you're using as an event listener.
Some frameworks have their own decoupling used for events. For example, jQuery attaches the event handler functions to a data lookup, indexed by an id that it drops into each Element object; this gives it decoupling for free, though it does have problems of its own...
If you're using plain JavaScript, here's some example helper code.
// Event binding with IE compatibility, and decoupling layer to prevent IE6-7
// memory leaks
//
// Assumes flag IE67 pre-set through eg. conditional comments
//
function EventTarget_addEventListener(target, event, listener) {
if ('addEventListener' in target) {
target.addEventListener(event, listener, false);
} else if ('attachEvent' in target) {
if (IE67)
listener= listener.decouple();
target.attachEvent('on'+event, listener);
}
}
Function.decouple_bucket= {};
Function.decouple_factory= function() {
function decoupled() {
return Function.decouple_bucket[decoupled.decouple_id].apply(this, arguments);
}
return decoupled;
};
Function.prototype.decouple_id= null;
Function.prototype.decouple= function() {
var decoupled= Function.decouple_factory();
do {
var id= Math.floor(Math.random()*(Math.pow(2, 40)));
} while (id in Function.decouple_bucket);
decoupled.decouple_id= id;
Function.decouple_bucket[id]= this;
return decoupled;
};
Function.prototype.release= function() {
delete _decouple_bucket[this.decouple_id];
};
if (IE67) {
EventTarget_addEventListener(window, 'unload', function() {
Function.decouple_bucket.length= 0;
});
}
// Add ECMA262-5 method binding if not supported natively
//
if (!('bind' in Function.prototype)) {
Function.prototype.bind= function(owner) {
var that= this;
if (arguments.length<=1) {
return function() {
return that.apply(owner, arguments);
};
} else {
var args= Array.prototype.slice.call(arguments, 1);
return function() {
return that.apply(owner, arguments.length===0? args : args.concat(Array.prototype.slice.call(arguments)));
};
}
};
}
With all that tedious plumbing out of the way, you can then just say:
function HelloObject(element) {
this.element= element;
EventTarget_addEventListener(element, 'click', this.hello.bind(this));
}
HelloObject.prototype.hello= function() {
alert('Hello, you clicked on a '+this.element);
};
that might lead to browsers going slow or to break???
No, it's only really memory leaks (and then mostly in IE) that we're worried about when talking about refloops.
The answer is in the body of the question. It seems weird to you because of your C++ background. The 2nd option is the Javascript-ish way of doing it.

Categories

Resources