7

An example would be if you had multiple inputs, each of which effect a completely different process, (perhaps one changes a div's width, while another sends ajax request for predictive search), would you bind a single event use a switch statement to call the process functions, or set multiple eventListeners bound to each input element?

Part of the reason for me asking is I'm not sure of the performance implications of multiple event listeners, but even if those are negligible, readability and code maintenance considerations could still impact on the answer.

Some considerations:

a) Would it depend on the expected frequency of use?

b) would a common event such as mousemove be better with one approach, while input be better with another?

c) if the switch approach is deemed better, what is the maximum size/complexity that the switch statement should be allowed to grow to.

9

The Buzz Word you are looking for, which might help in your search for answers, is JavaScript Event Delegation.

There are two basic kinds of event delegation:

  • Component delegation: One listener for each event is registered per component on the page

  • Front Controller delegation: One listener for each event is registered for all components, and then attributes within the HTML are used for dynamic dispatch to functions or objects representing each component.

The kind you are questioning is the Front Controller variety.

Front Controller Event Delegation

A working example of this is a library I wrote called Oxydizr. It uses a custom HTML attribute data-actions to dispatch the handler to the proper "controller" object. While not a switch statement, it boils down to the same basic logic as you are proposing.

This layer of abstraction is useful if you have an unknown number of components that will be used on each page, and you don't want to write additional JavaScript code to wire up your UI to event listeners.

The increase in complexity is balanced out by the ease of extensibility and implementation. No more do you need to write HTML code and a bunch of JavaScript code specific to that HTML structure. Add a splash of HTML attributes and event delegation on a common root element handles calling the proper function.

a) Would it depend on the expected frequency of use?

Yes. If you have a lot of events to handle, or a lot of elements for which you need to handle interactions, the Front Controller flavor of event delegation is a good fit. The added benefit here is, if done right, adding new components is very easy.

b) would a common event such as mousemove be better with one approach, while input be better with another?

Actually, since the mousemove event can fire upwards of 50 times per second. To maintain a smooth experience for the user your Front Controller and handler must execute faster than 20 milliseconds. That's not much time for an interpreted language! For that reason, "spammy" events like mousemove and scroll are better handled on their own outside of event delegation of any sort. This leads into the performance implications of this pattern.

Event Delegation Performance

You do take a hit to performance at runtime, but gain performance at page load. The more nodes in the document tree, the more apparent the gain in page load performance becomes.

Libraries like jQuery, and native DOM API's like querySelectorAll allowing you to target elements by CSS selector have a hidden dark side: unless the browser optimizes for certain selectors, the browser basically has to iterate over every element in the document tree when getting elements by CSS selector. This is especially noticeable at page load for larger pages, and becomes even more noticeable on mobile devices with slower processors or less RAM. So, code like:

var elements = document.querySelectorAll(".foo");

for (var i = 0; i < elements.length; i++) {
    elements[i].addEventListener(...);
}

Gets increasingly costly in execution time as your web application grows.

It is much faster at page load to add listeners to one element. My favorite is document.documentElement, because it references the <html> element, and exists from the moment JavaScript begins executing. No need to wait for a DOMContentLoaded event or "DOM ready" event.

You do incur a runtime performance hit for two reasons:

  1. The event is handled after bubbling up from the source element, rather than being handled at the source element. It's been my experience that this is minimal, if not so small you can't accurately measure the impact in JavaScript.

  2. The event delegate, be it a Front Controller or an individual component, must loop in JavaScript from the source element up the document tree for each invocation of the event just to see if it is something it can handle.

These two items do reduce performance while the user is interacting with the page. Again, it's a small reduction, so avoiding event delegation for these two reasons is a Premature Optimization.

Dynamic Dispatch with a Front Controller

c) if the switch approach is deemed better, what is the maximum size/complexity that the switch statement should be allowed to grow to.

The mere fact you are questioning this tells me that a switch statement is the wrong way to go about this. Don't use a switch statement. You need to annotate your HTML with some custom data-* attributes to make extending this easy, and cost zero work.

<button type="button"
        data-action="confirm"
        data-confirm="Are you sure?">
    Delete Item?
</button>

In this naive implementation you would register a listener on a "Front Controller":

frontController.addListener("confirm", function(event) {
    if (!confirm(event.target.getAttribute("data-confirm")) {
        event.preventDefault();
    }
});

Extending this for another component is easy:

frontController.addListener("removeItem", function(event) { ... });

The implementation of this example:

var frontController = {
    listeners: {},

    addListener: function(actionName, callback) {
        this.listeners[actionName] = callback;
    },

    handleClick: function(event) {
        var listeners = frontController.listeners,
            action = event.target.getAttribute("data-action");

        if (listeners[action]) {
            listeners[action](event);
        }
    }
};

document.documentElement.addEventListener("click", frontController.handleClick, false);

You'll see that there is no need for a switch statement, because data embedded in the HTML document allows you to write one decision structure to handle all components. This is where you want to be if implementing the Front Controller pattern for event delegation.

So, Should I reduce event listeners by making functions more complex?

Yes, if:

  • You expect to add many more components
  • You don't want to write code to add event listeners
  • You don't want to write code to remove event listeners for elements that have been removed from the document tree (unless you like memory leaks)

No, if:

  • You don't need to do a lot of JavaScript work
  • You don't mind writing code to add event listeners when elements are added to the page dynamically
  • You don't care about memory leaks from removed elements that still have active event listeners.

    And yes, there are times when you don't care about memory leaks. If the page isn't used for very long, a memory leak might not be a big deal. For Single Page Applications... I hope you have LOTS of RAM.

  • Great summary. I'd like to add that for querySelectorAll we don't have to look for the entire tree. We could use the .closest method on the event.target object, which accepts a similar if not identical query selector as document.querySelectorAll(".foo"); but only looks for the match in the target parents and self. developer.mozilla.org/en-US/docs/Web/API/Element/closest – Vlad Nicula Jun 27 at 7:38
  • It looks like it boils down to RAM, but how much RAM say 100 simple event listeners would approximately use? 1mb? 100mb? 1gb? – Arthur Tarasov Jul 21 at 5:32
3

I've never used switch for this particular scenario but at a glance, it seems redundant.

You are essentially adding a layer between the event and the handler function which does nothing else than call the handler function - not useful.

This brings me to the use case where using switch MIGHT make sense - for example, if you have to run some code prior to calling the handler functions. That could be a cleaner design compared to calling that piece of code from each of the handler functions.

1

I wouldn't go for a single listener, unless that listener itself is designed to be extensible, because it would violate the Open-Closed Principle, would lead to uncohesive features being packed together and would potentially break encapsulation by suggesting to leak internal details of sub-components into the "parent".

If we take very simple example where we have a feature A which is composed of features B & C.

With a single non-extensible listener approach we may be forced toward such a design:

function initFeatureA(domEl) {
    initFeatureB(domEl);
    initFeatureC(domEl);

    domEl.addEventListener('change', function (e) {
        if (...) doSomethingRelatedToFeatureA();
        else if (...) doSomethingRelatedToFeatureB();
        else if (...) doSomethingRelatedToFeatureC();

        //#1. OCP violation because as we add features we need to change the handler
        //#2. Not cohesive because we might have a very large spectrum of unrelated behaviors

        //#3. The design invites us to leak logic of sub-features into feature A
    });
}

function initFeatureB(domEl) {
    //...
}

function initFeatureC(domEl) {
    //...
}

Compare with the following, where every feature/component is better encapsulated:

function initFeatureA(domEl) {
    initFeatureB(domEl);
    initFeatureC(domEl);

    domEl.querySelector('.feature-a-input').addEventListener('change', function (e) {
        doSomethingRelatedToFeatureA();
    });
}

function initFeatureB(domEl) {
     domEl.querySelector('.feature-b-input').addEventListener('change', function (e) {
        doSomethingRelatedToFeatureB();
    });
}

function initFeatureC(domEl) {
     domEl.querySelector('.feature-c-input').addEventListener('change', function (e) {
        doSomethingRelatedToFeatureC();
    });
}

Now, there may be cases where a single handler makes more sense than multiple ones, but that single handler is generally designed to be extensible in order to avoid some of the concerns mentionned above. For instance, imagine a game where individual keys may bind to actions.

You could potentially design an InputController component which is responsible for abstracting away the low-level details of capturing the keys that were pressed. Then, other components may register themselves through the InputController as they need to perform actions.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.