Most efficient way to dynamically bind event handlers
Asked Answered
R

1

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

Ramayana answered 28/8, 2014 at 15:39 Comment(2)
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.Compartmentalize
Backbone.js has a very elegant way of doing it. See View-delegateEvents.Felicio
C
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.

Cobbler answered 28/8, 2014 at 16:34 Comment(9)
You say: "It represents the <html> element on the page, which holds everything". This isn't quite true as document and window are not inside of the <html> element. If you delegate all event handling to document.documentElement you'll miss events like popstate, scroll, DOMContentLoaded, etc.Stopper
@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:Ramayana
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.Ramayana
@wvandall: So jQuery is the performance bottleneck, not delegating events on the <html> element.Cobbler
@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.Cobbler
@PhilipWalton: Browsers actually scroll either the <body> or <html> 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.Cobbler
@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.Stopper
@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 costRamayana
@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.Cobbler

© 2022 - 2024 — McMap. All rights reserved.