1

When I am using on() to listen to event, I find the event handler is called more than once. Code as below:

html:

<div id="container">
<div>
    <div><input type="text"/></div>        
</div>    
</div>

JS:

var $div = $('#container'), $input = $('input');

$div.on('validate', 'div', function() {
    console.log('div');
});

$input.blur(function() {
    $(this).trigger('validate');
});

Here is the updated DEMO

  • Then the 'validate' handler is called twice.
  • When I dig into JQuery source code, I find that JQuery will find the parent node of event target and check the seletor matched or not.
  • If matched the selector, the handler queue will be pushed one more the handleobj. In the example, between event target and listener element div#contianer, there are two hierarchical divs.
  • In result, the handler queue has two event handler which are the same event handler function instance.

My question is: How to use selector correctly to prevent event handler be called more than once? Thanks.

I think to answer this question, we should carefully read the related source code in JQuery:

// Determine handlers that should run if there are delegated events
        // Avoid non-left-click bubbling in Firefox (#3861)
        if ( delegateCount && !(event.button && event.type === "click") ) {

            // Pregenerate a single jQuery object for reuse with .is()
            jqcur = jQuery(this);
            jqcur.context = this.ownerDocument || this;

            for ( cur = event.target; cur != this; cur = cur.parentNode || this ) {

                // Don't process events on disabled elements (#6911, #8165)
                if ( cur.disabled !== true ) {
                    selMatch = {};
                    matches = [];
                    jqcur[0] = cur;
                    for ( i = 0; i < delegateCount; i++ ) {
                        handleObj = handlers[ i ];
                        sel = handleObj.selector;

                        if ( selMatch[ sel ] === undefined ) {
                            selMatch[ sel ] = (
                                handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel )
                            );
                        }
                        if ( selMatch[ sel ] ) {
                            matches.push( handleObj );
                        }
                    }
                    if ( matches.length ) {
                        handlerQueue.push({ elem: cur, matches: matches });
                    }
                }
            }
        }
Ian Jiang
  • 413
  • 7
  • 7
  • Add code example link: http://jsfiddle.net/ianjiang/egcVb/ – Ian Jiang Jul 06 '13 at 08:03
  • 1
    I presume what you're seeing is event delegation/propagation. Some events propagate up the DOM hierarchy to parents. You can stop propagation with `event.stopPropagation()`. – jfriend00 Jul 06 '13 at 08:08
  • I don't know why, but if you do a `return false;` after the `console.log` statement it only logs once. – Erik Schierboom Jul 06 '13 at 08:09
  • [Demonstration of the problem.](http://jsfiddle.net/egcVb/2/) – lonesomeday Jul 06 '13 at 08:13
  • Yes, I realize JQuery custom event by trigger can be bubble up. But this is by design? Once the selector is matched, the handler queue will be added one more same handler. – Ian Jiang Jul 06 '13 at 08:28
  • Your code essentially means 'call the event handler for every div within the container when a `validate` event is fired within the container'. Thats exactly what the [docs](http://api.jquery.com/on/#on-events-selector-data-handlereventObject) say. No need to dig into the source code. – a better oliver Jul 06 '13 at 10:11

3 Answers3

0

$input.blur(function() {$(this).trigger('validate');});' Only input triggers validate event. So I don't think means "work for onblur for every elements on DOM

This is wrong because trigger bubble up the DOM tree, and triggers the same event for parent elements.

From DOCS

As of jQuery 1.3, .trigger()ed events bubble up the DOM tree; an event handler can stop the bubbling by returning false from the handler or calling the .stopPropagation() method on the event object passed into the event

So there are two divs inside the div#container,

$input.blur(function() {$(this).trigger('validate');});

also triggers for those two divs.

Here is an example that you can see which element is currentTarget,

http://jsfiddle.net/egcVb/4/

$div.on('validate', 'div', function(e) {
    console.log(e.currentTarget);
});

If you want to stop bubbling, just return false from that event.

http://jsfiddle.net/egcVb/5/

$div.on('validate', 'div', function(e) {
    console.log(e.currentTarget);
    return false;
});
Okan Kocyigit
  • 13,203
  • 18
  • 70
  • 129
0

This means: bind 'validate' event on $div, but include it for each div descendant too.

$div.on('validate', 'div', function() {
    console.log('div');
});

See jQuery on, parameter selector:

A selector string to filter the descendants of the selected elements that trigger the event. If the selector is null or omitted, the event is always triggered when it reaches the selected element.

Use this:

$div.on('validate', function() {
    console.log('div');
});

...and you will se just one event handled.

OzrenTkalcecKrznaric
  • 5,535
  • 4
  • 34
  • 57
  • Yes, but it doesn't mention the handler queue will be added one more same handler function instance. Does it by design? – Ian Jiang Jul 06 '13 at 08:30
  • I don't understand `the handler queue will be added one more same handler function instance`, but jQuery will by design pass through all filtered children and logically attach event handlers to them. As a result you will get 2 events handled. Try to differentiate those two inner div's by some class name and include that in `on` selector and you will see. – OzrenTkalcecKrznaric Jul 06 '13 at 08:34
  • `handlerQueue.push({ elem: cur, matches: matches });` You can see the related source code for dispatch function in JQuery. – Ian Jiang Jul 06 '13 at 09:01
0

If you have a good reason to bind the event handler to the div instead of the input, you can change your event handler to look like this:

$div.on('validate', '*', function(event) {
    var isRelevant = false;
    $(this).children().each(function(){
        if (this === event.target){
            isRelevant = true;
        }
    });

    if (isRelevant){
        console.log('div');
    }
});

This snippet checks if the event.target from which the event emanates (doc), is one of the direct children of the element the event has propagated to. isRelevant will only be true for the inner div.

You can check it out also in a fork I made off your example: http://jsfiddle.net/BwseN/

Naor Biton
  • 952
  • 9
  • 14