1
\$\begingroup\$

I have been working on a Backbone.js application, and I am starting to think I am making a huge mistake.

I never fully understand garbage collecting, and how exactly some closures prevent it. I just try to keep as organized as possible.

I just read an article that states that JavaScript variables that are not declared using the var keyword will not be removed from memory.

Now in my application I create everything inside an object called once. I clean this object every time I move to another page. I clean out the whole object, and remove all the elements and events associated to it.

Note: I am talking here about a single page backbone.js application. With changing pages I mean changing from one route to another using Backbone.History.navigate.

Am I making good use of the once Object inside the App object? Will all be gone when I change routes?

////////////////////////////////////
//
// Main Object for my application
//  - once : One time models/views/etc
//  - move : Called before I move to a
//           new route
//  - clean: Called after I moved to
//           the new route
//  - close: Used internal by `clean`
//  
/////////////////////////////////////
App = {
    once: {},

    move: function(){
        this.olds = this.once;
        this.once = {};
    },
    clean: function(){
        for(var Obj in this.olds){
            if(this.olds[Obj].models){
                this.olds[Obj].forEach(function(model){
                    this.close(model);
                }, this);
            }
            this.close(this.olds[Obj]);
        }

        delete this.olds;
    },
    close: function(Obj){
        if(typeof(Obj.onClose) !== 'undefined'){
            Obj.onClose();
        }
        if(typeof(Obj.off) !== 'undefined'){
            Obj.off();
        }
        if(typeof(Obj.remove) !== 'undefined'){
            Obj.remove();
        }
    }
}

///////////////////////////////////////////////
//
// My model, collection, and 2 views
//
// Note that I left out some basic steps
//
//////////////////////////////////////////////
window.User = Backbone.Model.extend({});
window.Users = Backbone.Collection.extend({
    model: User
});

window.UserView = Backbone.View.extend({
    // Init, events,  etc ...
    render: function(){
        this.collection.forEach(function(user){
            App.once.userSubView = new UserSubView({
                model: user
            });
            this.$el.append(App.once.userSubView.el);
        })
    }
});
window.UserSubView = Backbone.View.extend({});


///////////////////////////////////////////////
//
// One of the routes of my router.
//
// All my routs begin with move() and end
// with clean().
//
//////////////////////////////////////////////

App.move();

App.once.users = new Users();

// Create the views
App.once.users.fetch({
    success: function(){
        App.once.userView = new UserView({
            collection: App.once.users
        })
        $('body').append(App.once.userView.el);
    }
})

App.clean();
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

This code looks mostly fine to me, when the .move method gets called, .once gets moved to .old and then when .clean happens, .old gets deleted (and eventually garbage collected unless a reference to it continues to exist somewhere else).

However I think you could do better by encapsulating each route in a function that takes care of .move and .clean for you.

Something like:

route(function () {
    App.once.users = new Users();

    App.once.users.fetch({
        success: function () {
            App.once.userView = new UserView({
                collection: App.once.users
            });
            $('body').append(App.once.userView.el);
        }
    });
});

where route is:

function route(fn) {
    App.move();
    fn();
    App.clean();
}

(but you probably already have a method like this in your code where these things would belong)

A better question: Why do you need this .once property anyway? It looks like it is just holding private data for the route. Why not make it private to the route then?

route(function () {
    var users = new Users();

    users.fetch({
        success: function () {
            $('body').append(new UserView({ collection: users }).el);
        }
    });
});

Garbage collection in JavaScript

(very simplified basic design ignoring tricks/special cases)

Every variable in javascript can be thought of as a reference to an underlying system object.

That is, in JS you have:

var a, b;

to the system there is:

var all = [ {data: a, declaredScope: function()}, {data: b, declaredScope: function()}];
var scope = function();

When the garbage collector runs, it does something like this:

function markDescend(variable) {
    for (var child in variable) { //the system has the child as seen in the "all" array
        if (child !== undefined && child !== null && !child.marked) {
            child.marked = true;
            markDescend(child);
        }
    }
}
var tempscope = scope;
var tempall = all;
do {
    for (var variable in tempall) {
        if(tempscope === variable.declaredScope) {
            variable.marked = true;
            markDescend(variable);
        }
    }
    tempscope = tempscope.outer;
} while (tempscope);

all = [];
for (var variable in tempall) {
    if(variable.marked) {
        variable.marked = false;
        all.push(variable);
    } else {
        __free(variable);
    }
}

That is to say, when the garbage collector runs, all variables reachable from the current scope (and closure scopes) are marked; then a second loop runs and all variables not marked are freed from memory.

Thus, if there is no way for the system to reach an object via scope then it is going to be collected. From a user perspective, there are a few ways to create objects that the user can not reach, but are still in scope. Some examples:

  • window.setTimeout(function () {}, x) - this function is reachable from global scope until it finishes running; and it has access to any variables from any of its closure scopes (even though you may not be able to reach it)
  • $.ajax('asdg', function () {}) - same as setTimeout
  • Any attached DOM node is reachable from the global scope. You may attach events to these nodes and if you don't maintain the reference, you would lose access to it.

In these cases, you would need to do more than what you are currently doing in your .move method to take care of unregistering these functions (In IE7 and below). My perspective would be just to let it leak (tell the users to get a better browser). IE6 would leak until the browser is closed, IE7 would leak until the page is out of the navigation cache. In every other browser, once the DOM is detached (or the timeout occurs, or the ajax post happens, etc.) these functions are not reachable from the global scope anymore (and so they are removed in the next garbage collection cycle).

Cleanup in IE

Essentially what you are doing in .move is move the stuff you want to delete in the previous route to .old and then in .clean you take care of deleting the old references.

Really the only things you should care about are those properties that you cannot actually get at via variable properties once set. Whenever you do a window.setTimeout() (or similar) you would want to do something like this:

function setTimeout(fn, msec, App) {
    var id = window.setTimeout(function () {
            fn();
            destructor.removeReference = null;
        }, msec),
        destructor = {
            removeReference: function () { window.clearTimeout(id); }
        };
    App.once.push(destructor);
}

function setInterval(fn, msec, App) {
    var id = window.setInterval(fn, msec);
    App.once.push({
        removeReference: function () { window.clearInterval(id); }
    });
}

and so on for attaching events, setting DOM properties, making ajax requests, etc. Then in .clean you would check if the object in .old has a removeReference function; if so run it then delete it and the object.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for commenting, good suggestions. I have added a response to your answer in the OP. \$\endgroup\$ Apr 30, 2012 at 17:18
  • \$\begingroup\$ Take note, I am trying to explain this so that anyone who can read JS can understand how this collection process works (there are many technicalities that I am glossing over). Another edit is coming with changes necessary to get around the DOM circular reference issue in IE. \$\endgroup\$
    – Bill Barry
    Apr 30, 2012 at 21:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.