Backbone.js is a deceptively simple framework. You can read the full documentation and get a grip on the source code in half a day. It weighs in at just 1,400 (sparse and well documented!) lines of code, and its production size is less than 20% of jQuery's.

Everything looks a little too easy.

In reality, as soon as you do anything non-trivial, you're likely to end up in unknown territory, where memory leaks and double renders abound! It's not hard to stay on top of these issues, but they're often ignored in introductory tutorials, and Backbone (intentionally) doesn't prescribe best practices for handling them.

This series of posts will detail how we manage the development of a non-trivial Backbone application: Paydirt.


Part I: Memory Management and Event Bindings

Javascript doesn't have any manual memory management, and objects you create can't be garbage collected until they are no longer referenced.

In practice, Backbone views are often referenced by the model they represent, via bindings on the model's events. As a result, they are unable to be garbage collected, even when the view's $el is removed from the DOM.

In this post we'll set up a three step system to ensure our Backbone views can be garbage collected, to ensure your application doesn't get sluggish or worse, crash the user's browser:

  1. Always store a reference to the current top-level view
  2. Keep track of every nested view
  3. Ensure every event binding gets unbound

1. Always store a reference to the current top-level view

In your router, you need to keep a reference to the current top-level view, so that you can clean it up when the user navigates. It's pretty straightforward to do this:

// Define your route as per usual
routes:
  'dashboard': 'dashboard'

// Each action populates @view, and calls @render()
dashboard: =>
  @view = new Paydirt.Views.Dashboard.Index()
  @render()

// Render stores the current view
render: =>
  @_currentView = @view
  $('#main').html(@view.render().el)

Every route action calls @render(), so we'll always have a reference to the current view. We'll come back to cleaning it up shortly.


2. Keep track of every nested view

A nested view is simply a view that is created and managed by a parent view. For example, you might have a List view, which is in charge of rendering many ListItem views.

An example rendering of Backbone nested views

Every view that instantiates other views needs to keep a reference to them so that it can clean them up later. Again, this isn't hard to do:

// Set up an object for keeping track of nested views
initialize: (options) ->
  @subViews = {}

render: ->
  // Do your rendering of the view however you normally would
  @$el.html(@template())

  // Render a ListItem view for each member of the collection
  @collection.each (timeLog) =>
      view = new Paydirt.Views.TimeLogs.ListItem(model: timeLog)
      // Keep a reference to each nested view as you instantiate them
      @subViews['timelog-' + timeLog.cid] = view
    @$el.append(view.render().el)

3. Ensure every event binding gets unbound

In Backbone, it's common practice to have your views update themselves when the model they represent changes.

For example, our invoice view is bound to the invoice's total changing. When a line item's value is changed, the invoice's total is recalculated, and the view updates the DOM to reflect the new total:

A Backbone view that is bound to a models events

To stay on top of memory leaks, every model binding from within a view needs to be explicitly unbound. The usual time to do this is when the view is removed:

// Bind to the model events we're interested in
initialize: (options) ->
  @model.on('change:total', @refreshTotal)

// Define a function to unbind all the model events we bound to in initialize()
teardown: ->
  @model.off('change:total', @refreshTotal)

Note that the view's delegated events automatically get unbound by jQuery when it's $el is removed from the DOM. If you need to unbind these before you remove the DOM element, you can always call @undelegateEvents() on the view.


Putting It All Together

Lets extend our view teardown() function to handle nested views:

teardown: =>
  // Call teardown on all nested views, to ensure their model events are unbound
  for viewName, view of @subViews
    view.teardown()
  // And unbind the current views model events, too
  @model.off('change:total', @refreshTotal)

While we're at it, lets abstract our event bindings to keep things DRY:

// Define our view's event bindings in one place, instead of in initialize() and teardown()
// This pattern is modelled after the view's events object
// You can define events on @model, @collection, or anything really
objectEvents:
  model:
    'change:subtotal change:total': 'refreshTotal'

Now we can write a utility function, _handleObjectEvents, that will call on() and off() on each of our objectEvents.

By extending Backbone.View, we don't have to call _handleObjectEvents() or define teardown() for every view:

class Paydirt.View extends Backbone.View
  // In initialize(), call .on() for each model event, and instantiate the @subViews object
  initialize: (options) =>
    @subViews = {}
    for object, events of @objectEvents
      @_handleObjectEvents('on', object, events)
    super(options)

  // In teardown(), call .off() for each model event, and teardown all of the view's subviews
  teardown: =>
    for viewName, view of @subViews
      view.teardown()
    for object, events of @objectEvents
      @_handleObjectEvents('off', object, events)
    @off()
    @remove()
    @undelegateEvents()
    this

  // Utility function to bind and unbind each of our objectEvents
  _handleObjectEvents: (binding, object, events) ->
    for event, callback of events
      unless _.isObject(object)
        object = @[object]
      // Handle bindings on nested objects
      if _.isObject(callback)
        @_handleObjectEvents(binding, object[event], callback)
      else
        // In our example, this would call model.on('change:subtotal change:total', @['refreshTotal'])
        object[binding](event, @[callback])

From now on, we'll extend Paydirt.View instead of Backbone.View to inherit this functionality automatically.

Finally, we need to update our router's render() method to call teardown() on the topmost view to trigger the recursive teardown:

// Tear down the existing view if there is one, then render the new one
render: =>
  if @_currentView?
    @_currentView.teardown()
  @_currentView = @view
  $('#main').html(@view.render().el)

Voila. Our router tears down the current view, which cleans up that view's events and recursively tears down any subViews that view might have had.

As a result, our memory leak problems are basically taken care of. All we have to do is remember to populate @subViews, and to bind to events using our objectEvents object, instead of doing so manually.


Coming up next...

In Part II of this series, we'll discuss keeping track of unsaved objects, and prompting the user to discard their changes if they navigate away.


Find time tracking hard to remember? Is invoicing a chore?

Paydirt is a time tracking and invoicing suite designed for freelancers and small teams that makes time tracking a breeze and invoicing a joy. Find out more.

Follow @paydirtapp on Twitter for more tips for freelancers.

Tags: paydirt javascript backbone-js tutorial


Time tracking and invoicing software