JQuery event model and preventing duplicate handlers

45,028

Solution 1

Prevent duplicate binding using jQuery's event namespace

There are actually a couple different ways of preventing duplicate. One is just passing the original handler in the unbind, BUT if it is a copy and not in the same space in memory it will not unbind, the other popular way (using namespaces) is a more certain way of achieving this.

This is a common issue with events. So I'll explain a little on the jQuery events and using namespace to prevent duplicate bindings.



ANSWER: (Short and straight to the point)


// bind handler normally
$('#myElement').bind('myEvent', myMainHandler);

// bind another using namespace
$('#myElement').bind('myEvent.myNamespace', myUniqueHandler);

// unbind the unique and rebind the unique
$('#myElement').unbind('myEvent.myNamespace').bind('myEvent.myNamespace', myUniqueHandler);
$('#myElement').bind('myEvent.myNamespace', myUniqueHandler);

// trigger event
$('#myElement').trigger('myEvent');

// output
myMainHandler() // fires once!
myUniqueHandler() // fires once!



EXAMPLE OF ANSWER: (Full detailed explanation)


First let's create an example element to bind to. We will use a button with the id of #button. Then make 3 functions that can and will be used as the handlers to get bound to the event:

function exampleOne() we will bind with a click. function exampleTwo() we will bind to a namespace of the click. function exampleThree() we will bind to a namepsace of the click, but unbind and bind multiple times without ever removing the other binds which prevents duplicating binding while not removing any other of the bound methods.

Example Start: (Create element to bind to and some methods to be our handlers)

<button id="button">click me!</button>


// create the example methods for our handlers
function exampleOne(){ alert('One fired!'); }
function exampleTwo(){ alert('Two fired!'); }
function exampleThree(){ alert('Three fired!'); }

Bind exampleOne to click:

$('#button').bind('click', exampleOne); // bind example one to "click" 

Now if user clicks the button or call $('#button').trigger('click') you will get the alert "One Fired!";

Bind exampleTwo to a namespace of click: "name is arbitrary, we will use myNamespace2"

$('#button').bind('click.myNamespace2', exampleTwo);

The cool thing about this is, we can trigger the "click" which will fire exampleOne() AND exampleTwo(), or we can trigger "click.myNamespace2" which will only fire exampleTwo()

Bind exampleThree to a namespace of click: "again, name is arbitrary as long as it's different from exampleTwo's namespace, we will use myNamespace3"

$('#button').bind('click.myNamespace3', exampleThree);

Now if 'click' get's triggered ALL three example methods will get fired, or we can target a specific namespace.

PUT IT ALL TOGETHER TO PREVENT DUPLICATE

If we were to continue to bind exampleThree() like so:

$('#button').bind('click.myNamespace3', exampleThree); 
$('#button').bind('click.myNamespace3', exampleThree);
$('#button').bind('click.myNamespace3', exampleThree);

They would get fired three times because each time you call bind you add it to the event array. So, really simple. Just unbind for that namespace prior to binding, like so:

$('#button').unbind('click.myNamespace3').bind('click.myNamespace3', exampleThree); 
$('#button').bind('click.myNamespace3', exampleThree);
$('#button').unbind('click.myNamespace3').bind('click.myNamespace3', exampleThree); 
$('#button').bind('click.myNamespace3', exampleThree);
$('#button').unbind('click.myNamespace3').bind('click.myNamespace3', exampleThree); 
$('#button').bind('click.myNamespace3', exampleThree);

If the click function is triggered, exampleOne(), exampleTwo(), and exampleThree() only get fired once.

To wrap it all together in a simple function:

var myClickBinding = function(jqEle, handler, namespace){
    if(namespace == undefined){
        jqEle.bind('click', handler);
    }else{
        jqEle.unbind('click.'+namespace).bind('click.'+namespace, handler);
    }
}   

Summary:

jQuery event namespaces allow for binding to main event but also allow child namespaces to be created and cleared without effecting sibling namespaces or parent ones which with very minimal creative thinking allows prevention of duplicate bindings.

For further explanation: http://api.jquery.com/event.namespace/

Solution 2

Ummm how about using one()? http://api.jquery.com/one/

Or am i completely misunderstanding you?

Solution 3

This isn't a full answer, but I quickly found how to fix all my code with minimal changes. In my "base" js class (this is an object which is used on every page for standard button hookups by class name etc) I added the following method which does the check for dupe handlers:

BindOnce: function(triggerName, fn) {
    function handlerExists(triggerName, theHandler) {
        function getFunctionName(fn) {
            var rgx = /^function\s+([^\(\s]+)/
            var matches = rgx.exec(fn.toString());
            return matches ? matches[1] : "(anonymous)"
        }
        exists = false;
        var handlerName = getFunctionName(theHandler);
        if ($(document).data('events') !== undefined) {
            var event = $(document).data('events')[triggerName];
            if (event !== undefined) {
                $.each(event, function(i, handler) {
                    if (getFunctionName(handler) == handlerName) {
                        exists = true;
                    }
                });
            }
        }
        return exists;
    }
    if (!handlerExists(triggerName, fn)) {
        $(document).bind(triggerName, fn);
    }
},

Then I just invoke it instead of the bind method!

$.mystuff.BindOnce("TheTrigger", OnTriggered)

Note that you can't use anon methods here as they would just be called 1, 2, 3 etc and the only way to check for dupes would be with a toString() on the method, which would be pretty slow in a complex application

Solution 4

About a hundred years too late, but if you're not using anonymous functions you can do this much more simply by using unbind first:

$.fn.eventWillOnlySubscribeOnce = function () {
    return this.each(function () {
        var oneHandler = (function () {
             HANDLER CODE
        });

    $(this).unbind("submit", oneHandler);
    $(this).bind("submit", oneHandler);
});
};

This implementation will work in Firefox 4, but not in many other browsers - because the handler variable is created new each time so the unbind can't find a matching handler to unbind. Moving the handler into the parent scope fixes the problem: i.e.

var oneHandler = (function () {
    HANDLER CODE
});

$.fn.eventWillOnlySubscribeOnce = function () {
    return this.each(function () {
    $(this).unbind("submit", oneHandler);
    $(this).bind("submit", oneHandler);
});
};

Presumably Firefox is doing some clever optimisation which means the handler is held as a constant variable somewhere as it never changes.

Share:
45,028
Mr AH
Author by

Mr AH

Updated on March 12, 2020

Comments

  • Mr AH
    Mr AH about 4 years

    Once again I want to load a page which contains its own script into a div using $("divid").load(...). The problem I face is related to events. Let's say we trigger("monkey") from the parent page and on the loaded page we bind("monkey") and just do an alert("monkey bound"). If the same load method is called multiple times, the bind is called multiple times. Now I could just unbind it before I bind it, or check the number of handlers before the bind and then not bind it to prevent this. Neither option is scalable as what if I later want to bind to that trigger in another "sub page" (a page loaded into a div).

    What I ideally want to do then is check if the handler I am about to add already exists, but I still WANT to use anonymous handlers... (asking a bit much with that last request I think). Currently I have a workaround by using pre-defined/named methods and then checking this before the bind.

    // Found this on StackOverflow
    function getFunctionName(fn)
    {
     var rgx = /^function\s+([^\(\s]+)/
     var matches = rgx.exec(fn.toString());
     return matches ? matches[1] : "(anonymous)"
    }
    
    function HandlerExists(triggerName, handlerName) {
            exists = false;
            if ($(document).data('events') !== undefined) {
                var event = $(document).data('events')[triggerName];
                if(event !== undefined)
                {
                    $.each(event, function(i, handler) {
                        alert(handlerName);
                        if (getFunctionName(handler) == handlerName) {
                            exists = true;
                        }
                    });
                }
            }
            return exists;
        }
    

    This is a pretty crude way of going about it I feel, but appears to work. I just do the following before the bind as follows:

    if (!HandlerExists("test", "theMethod")) {
        $(document).bind("test", theMethod);
    }
    

    Does anyone have a more elegant solution? for instance, is there any way to check a particular script is loaded? so I could use getScript() to load the js from the child page on first load, and then simply not load it on subsequent loads (and just fire a trigger which would be handled by he preexisting js)..

  • karim79
    karim79 over 14 years
    It crossed my mind too, but one will only happen once per each bound element and handler injected into the page, and I'm pretty sure those handlers should stay bound while said elements are in view.
  • Mr AH
    Mr AH over 14 years
    I was not aware of this method! very useful. Though it wont be useful for many examples in my project. The js is already loaded (and big) and so loading and unbinding it every time may cause further memory issues. Also as karim79 suggests many of the handlers do need to stay bound. Thanks for pointing me in this direction though, I'm gonna try using that for the on sub form loaded handlers and see if we suffer leakage.
  • hswner
    hswner almost 9 years
    I think there is a mistake. You unbind and bind 'click.myNamespace3' in one line. Then on the next line you bind again 'click.myNamespace3'. So, when the visitor clicks on the button, exampleThree() should be called twice, not once. Here's the proof: jsfiddle.net/mpr79qm8
  • Throttlehead
    Throttlehead over 8 years
    Namespacing the events works great! Much easier than coding your own custom event bus when you dont need or want something that heavy or you're working with external events that are fired on the window level, such as with Flash and Actionscript. They should really make this easier to find in the docs.
  • Abhinav Galodha
    Abhinav Galodha over 7 years
    $(document).data('events') wouldn't work for enumerating the events attached to an element as of jquery <= 1.8