5

As we all know, since jQuery 1.7:

$('someSelector').live('click', fn());

has become, essentially:

$(document).on('click', 'someSelector', fn());

All live events aren't directly bound to the elements in the selector, but delegate bound to the document.

This, I assume, is because elements that would match 'someSelector' in the future, aren't present in the DOM, so can't have event handlers bound (via direct or delegate binding).

For single page applications where the vast majority, if not all elements are dynamically loaded, are there published guidelines about how best to handle the issue of performance with binding everything to the document?

Covering, for instance, the best way to register/re-register event handlers when new content is loaded via ajax() and how to update code written in the lazy .live() mindset?

StuperUser
  • 10,555
  • 13
  • 78
  • 137
  • I usually bind to either the document or a parent container that doesn't get removed. Re-registering the events after ajax requests is an option, however it can become difficult to maintain. – Kevin B May 01 '12 at 17:19
  • @Chad I like your style. I will say "boom" while doing so and then "mic drop" my mouse. – StuperUser May 01 '12 at 17:21
  • 1
    But in all seriousness there isn't anything "wrong" with binding to the `document` unless it is content that you think will never come back once it is gone, bind to the document. (that is `$(document).on()` not `$(document).bind()` obviously) – Chad May 01 '12 at 17:23
  • 1
    @Chad: The downside is the overhead of running every selector bound against every element on the page that receives a matching event. If you have 20 `click` handlers/selectors bound to the document, every `click` on the page needs to run all 20 selectors provided to see if there's a match. And it doesn't just run each selector for the `target` element, but I'm pretty sure it runs on each ancestor of the target met until the `document` is reached. –  May 01 '12 at 17:28
  • @amnotiam How is that balanced against the running of code to ensure registration is rebound "cheaper" after each GET/POST? – StuperUser May 01 '12 at 17:31
  • @StuperUser: I wouldn't want to do that either. I would do what I could to reuse elements instead of destroying them. But that's not always convenient, so as a compromise, I'd bind the handlers to the nearest reliably available ancestor. I assume there's some element that contains the part of the page that is being renewed. I would bind to that so it only handles events local to it. –  May 01 '12 at 17:34
  • @amnotiam I think it only checks at the target element's bubble propagation of the event, checking at each level regardless of the target makes no sense. And since these events will be propagating to the document anyway, what is the difference? You check after bubbling instead of before, there isn't much if any difference in performance there. – Chad May 01 '12 at 18:13
  • @Chad: The event has bubbled to the `document` before the handler is ever invoked. And no, not only the `target` is checked, so jQuery must start with the `e.target`, and traverse the ancestors testing each selector on each one. This can easily be demonstrated... http://jsfiddle.net/4keKL/ Notice that there's no handler bound to the target you click, and yet the handlers on both ancestors are invoked. This could only happend if each selector was run against each ancestor element along the way to the `document`. –  May 01 '12 at 18:19
  • @Chad Not true. Only if every event triggered would also be handled, you would be right. But that's not the case. Invoking an event handler function might be fast, but it is not free. The more event handlers you bind to `document`, the more of them would have to be invoked unnecessarily. jQuery optimizes for that case, but still the complete list of selector expressions would have to be checked for every event handler for every event. Not piling these checks up at the `document` is a good thing - if not for performance, then at least for clean code. It's a bit like avoiding global variables. – Tomalak May 01 '12 at 18:21
  • 1
    Maybe I am misunderstand both of you but [this jsPerf](http://jsperf.com/doc-vs-local-ancestor) would suggest binding to document is faster... – Chad May 01 '12 at 18:38
  • 1
    @Chad Your jsPerf test checks for *event binding* speed, not *event handling* speed. – Tomalak May 01 '12 at 18:41
  • @Tomalak The setup binds to the document, then the test triggers the click event which calls the handlers. How does that not test handling speed? – Chad May 01 '12 at 20:11
  • @Chad That's right, my bad. It was late yesterday. ;) However, here's the opposite case: http://jsperf.com/doc-vs-local-ancestor/2. I've equalized things a little and I've used native click events to minimize the influence of jQuery itself on the test case. – Tomalak May 02 '12 at 05:29
  • Well ***obviously*** it will be faster if you stop propagation. My entire point though was that if you ***do not*** stop propagation, binding to the document if more efficient because it is going there anyway! If I change nothing except to allow propagation (comment out `e.stopPropagation()`) then document is more performant. [see here](http://jsperf.com/doc-vs-local-ancestor/4) – Chad May 02 '12 at 12:07
  • @Chad I'm not sure what jsPerf does there, but the number of click events that register at `document` level is consistently the lowest, even though it has the highest "ops/second" count. (http://jsperf.com/doc-vs-local-ancestor/6, look at the console). So either I'm mis-interpreting something here or jsPerf isn't the right tool to test this. – Tomalak May 03 '12 at 15:36

1 Answers1

3

I'm not sure whether there are "published guidelines".

I think this approach has its merits:

  • Find the closest logical common ancestor that will not be deleted from the document.

    Example: For draggable table row behavior, this would be the parent table (or tbody).

  • Bind events there. This allows you to have separate instances of the same thing behave differently without the need for a context check.

    Example: If row-dragging is disabled temporarily on one of your tables, the table that handles the event would know, naturally. The document would not.

  • It would keep the number of events that are handled by document at a minimum, so there would not be many "is this really necessary" checks when an event occurs.

    Example: If there currently is no table with draggable rows, there is no need that an event handler even fires at the document level (just to find out that table.draggable tr wasn't really the source of the event and dismiss it immediately).

  • If the common ancestor gets removed frequently, you can decide whether re-binding the event handlers upon its creation or binding them a few levels up in the hierarchy would be better.

I would re-bind container events upon container creation, but that's personal preference, I presume. Re-binding is easy enough, after all:

// once, beforehand
var draggableTableRowBehavior = {
  dragstart: function () { /* ... */ },
  dragstop: function () { /* ... */ }
  /* ... */
};

//in Ajax success:
$table.on(draggableTableRowBehavior, 'tr');
Tomalak
  • 332,285
  • 67
  • 532
  • 628