Backbone events firing twice

Garrett picture Garrett · Jun 11, 2012 · Viewed 10.2k times · Source

Why must I create a whole close prototype just to have my events unbinded from my view? Shouldn't Backbone just build that in? Is there a way to detect when a view is being removed?

My backbone events fire twice after I navigate away and back to the view.

        events : {
            "click #userDropdownButton > a" : "toggleUserDropdownMenu",
            "click" : "hideUserDropdownMenuIfClickedOutside"
        },

        el : "body",

        initialize : function() {
            this.render();
        },

        // Shows/hides the user dropdown menu
        toggleUserDropdownMenu : function() {
            if(!this.$el.find('#userDropdownButton > ul').is(':visible')) {
                this.showUserDropdownMenu();
            } else {
                this.hideUserDropdownMenu();
            }
            return false;
        },
        showUserDropdownMenu : function() {
            this.$el.find('#userDropdownButton').addClass('hover');
            this.$el.find('#userDropdownButton > ul').show();
            this.$el.find('#userDropdownButton > a .arrow-down').removeClass('arrow-down').addClass('arrow-up');
        },
        hideUserDropdownMenuIfClickedOutside : function() {
            if(this.$el.find('#userDropdownButton > ul').is(':visible')) {
                this.hideUserDropdownMenu();
            }
        },
        hideUserDropdownMenu : function() {
            this.$el.find('#userDropdownButton').removeClass('hover');
            this.$el.find('#userDropdownButton > ul').hide();
            this.$el.find('#userDropdownButton > a .arrow-up').removeClass('arrow-up').addClass('arrow-down');
        },

The first time the view is rendered, the dropdown opens and closes properly, but on the second time the view is rendered, the dropdown interprets every click twice, so as soon as it opens, the second click closes it.

Answer

B Robster picture B Robster · Jun 12, 2012

Update 2013/05/01: Backbone 0.9.9+ has added some built-in functionality to facilitate easy dealing with teh zomg problem (see View.remove and StopListening); but you'll still need to kill your zombies by calling one of these.


Why must I create a whole close prototype just to have my events unbinded from my view?

Derick's article is awesome at covering this whole issue. But I can add my two bits, addressing your question of "why" its not built in.

Because of the way that Backbone event delegation works, views won't be garbage collected when they go out of scope if they have event bindings. This is because the objects whose events they bind to--Backbone objects, in the case of binding to Backbone object events, or the DOM event system in the case of "events" callbacks--maintain references to the view's functions.

Believe it or not, some Backbone users rely on this behavior and expect views to continue auto-responding to events like they were told to do, even if they have gone completely out of scope. (I've seen several tutorials that do this. ) This assumes that you never need to remove the view, or that the view can respond to events and remove itself, since you've lost any reference to it, but IMO, this 'create and forget' functionality is nice as long as you understand the implications.

mu is too short makes a good point about UI events. Removing the el from the DOM should remove the delegated events as well. The same can't be said for binding to model or collection events or to other Backbone objects' events (any object can extend the Backbone Events prototype). You'll need to follow Derick Bailey's advice in the article you link to and manually close the view in these cases. I am not sure if this is a weakness of Backbone compared to other JS MVC frameworks, I haven't tried others as in depth.

"Is there a way to detect when a view is being removed?"

Not directly, that I'm aware of. But generally, whichever code is removing view should also clean up the event bindings if it needs done. Often, in good MVC architectures, views can set up an event observer on a corresponding model or collection, and then remove & cleanup themselves based on the event occuring (e.g., the "remove" event from a corresponding model). If you want to make your views' removal universally "detectable", one way would be to override the remove function in your own view prototype and trigger a custom event that can be observed by others.