8

Problem: I need to bind any number of event handlers to any number of elements (DOM nodes, window, document) at dynamically runtime and I need to be able to update event binding for dynamically created (or destroyed) nodes during the lifetime of my page. There are three options that I can see for tackling this problem:

I) Event delegation on window
II) Direct event binding on each node
III) Event delegation on common ancestors (which would be unknown until runtime and would potentially need to be recalculated when the DOM is altered)

What is the most efficient way of doing this?

A little context

I am working on a set of pages that need analytics tracking for user events (clicks, scrolling, etc.) and I want to be able to easily configure these event handlers across a bunch of pages without needing to write a script to handle the event binding for each instance. Moreover, because I may have the need to track new events in the future, or to track events on elements that are dynamically added to/removed from the page, I need to be able to account for changes in the DOM that occur during the lifetime of the page.

As an example of what I'm currently considering, I would like to create a function that accepts a config object that allows the programmer to specify default handlers for each event, and allow them to override them for specific elements:

Analytics.init({
    // default handlers for each event type
    defaultHandlers: {
        "click": function(e) { ... },
        "focus": function(e) { ... }
    },

    // elements to listen to 
    targetElements: {

        // it should work with non-DOM nodes like 'window' and 'document'
        window: {
            // events for which the default handlers should be called
            useDefaultHandlers: ['click'],

            // custom handler
            "scroll": function(e) { ... }
        },

        // it should work with CSS selectors
        "#someId": {
            useDefaultHandlers: ['click', 'focus'],
            "blur": function(e) { ... }
        }
    }
});

Sources

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
wvandaal
  • 4,265
  • 2
  • 16
  • 27
  • I think it depends. If you want to handle most of the events that reach `document`, you could delegate to it. But if you only handle a small fraction, and specially if the events fire a lot (e.g. `mousemove`, `scroll`), better delegate to a nearer ancestor. I wouldn't bind to each element if there are lots of them. – Oriol Aug 28 '14 at 16:28
  • Backbone.js has a very elegant way of doing it. See [View-delegateEvents](http://backbonejs.org/#View-delegateEvents). – tcooc Aug 28 '14 at 18:41

1 Answers1

1

I usually delegate events on the document.documentElement object because:

  1. It represents the <html> element on the page, which holds everything which holds all the HTML tags the user can interact with.
  2. It is available for use the moment JavaScript starts executing, negating the need for a window load or DOM ready event handler
  3. You can still capture "scroll" events

As for the efficiency of event delegation, the more nodes the event must bubble up the longer it takes, however we're talking ~1 to 2 ms of time difference -- maybe. It's imperceptible to the user. It's usually the processing of a DOM event that introduces a performance penalty, not the bubbling of the event from one node to another.

I've found the following things negatively affect JavaScript performance in general:

  1. The more nodes you have in the document tree, the more time consuming it is for the browser to manipulate it.
  2. The greater the number of event handlers on the page the more JavaScript slows down, though you would need 100s of handlers to really see a difference.

Mainly, #1 has the biggest impact. I think trying to eek out a performance boost in event handling is a premature optimization in most cases. The only case I see for optimizing event handling code is when you have an event that fires multiple times per second (e.g. "scroll" and "mousemove" events). The added benefit of event delegation is that you don't have to clean up event handlers on DOM nodes that will become detached from the document tree, allowing the browser to garbage collect that memory.

(From the comments below) wvandell said:

The performance costs of event delegation have little to do with the actual 'bubbling' of events ... there is a performance hit incurred when delegating many selectors to a single parent.

This is true, however let's think about the perceived performance. Delegating many click events won't be noticeable to the user. If you delegate an event like scroll or mousemove, which can fire upwards of 50 times per second (leaving 20 ms to process the event) then the user can perceive a performance issue. This comes back to my argument against premature optimizations of event handler code.

Many click events can be delegated with no problem on a common ancestor, such as document.documentElement. Would I delegate a "mousemove" event there? Maybe. It depends on what else is going on and if that delegated "mousemove" event feels responsive enough.

Greg Burghardt
  • 17,900
  • 9
  • 49
  • 92
  • You say: "It represents the element on the page, which holds everything". This isn't quite true as `document` and `window` are not inside of the `` element. If you delegate all event handling to `document.documentElement` you'll miss events like `popstate`, `scroll`, `DOMContentLoaded`, etc. – Philip Walton Aug 28 '14 at 17:11
  • **@GregBurghardt** I think some of what you've written misses the point of the question and/or misrepresents the realities of event delegation. The performance costs of event delegation have little to do with the actual 'bubbling' of events (as you correctly noted). However, there is a performance hit incurred when delegating many selectors to a single parent. From the jQuery [.on() docs](http://api.jquery.com/on/): – wvandaal Aug 28 '14 at 17:40
  • *Attaching many delegated event handlers near the top of the document tree can degrade performance. Each time the event occurs, jQuery must compare all selectors of all attached events of that type to every element in the path from the event target up to the top of the document. For best performance, attach delegated events at a document location as close as possible to the target elements. Avoid excessive use of document or document.body for delegated events on large documents.* – wvandaal Aug 28 '14 at 17:41
  • @wvandall: So jQuery is the performance bottleneck, not delegating events on the `` element. – Greg Burghardt Aug 28 '14 at 18:47
  • @PhilipWalton: I should edit my post. I'm using the term "holds everything" very loosely. It holds all the HTML tags that the user can interact with. – Greg Burghardt Aug 28 '14 at 18:49
  • @PhilipWalton: Browsers actually scroll either the `` or `` element. You can attach a scroll event handler on the `document` because the scroll event bubbles up from the BODY or HTML node to the document node. I haven't done much with `popstate`, so I can't vouch for that. And DOMContentLoaded is irrelevant because your events are delegated to an element that exists as soon as JavaScript starts executing. – Greg Burghardt Aug 28 '14 at 18:50
  • @GregBurghardt saying `scroll` was a mistake. I was thinking of the function `window.scrolTo()` which is only on the window, but is not an event (obviously). However, the larger point was that if you're abstracting an event system for everything, you should delegate to `window` instead of `documentElement`. – Philip Walton Aug 28 '14 at 19:27
  • @GregBurghardt no..., event delegation is the process of listening to events that bubble up through the DOM and then filtering them through some series of conditionals (usually a list of css selectors) so that you can attach one handler to a parent element instead of specific handlers to each element. Even without jQuery, this is how event delegation works. That being said, most people practically choose to use jQuery since it handles the filtering process for you. If you read the first source I linked to in my answer, you'll see that it is the filtering that incurs the greatest perf cost – wvandaal Aug 28 '14 at 19:33
  • @wvandaal: I agree that the filtering incurs the performance cost. That being said, is this reduction in performance _actually_ noticeable by the user? If not, then just delegate it to whichever node you see fit. Unless there is a performance problem, the OP's question is largely moot. The question really should be, "What is the most maintainable method that is quickest to program." Once a performance problem is found, _then_ solve the one performance problem by attaching events in a different manor or on a different node. – Greg Burghardt Aug 28 '14 at 20:56