A few simple tips and tricks to create more robust JavaScript event handlers with better performance and fewer interactions. Learn how to subscribe multiple functions to the same event, prevent event flicker, and pass parameters to event handlers.
Before we can get into advanced event handling, here is a basic introduction to JavaScript events.
Advanced JavaScript programmers can skip this section to get right to the fun stuff.
Let's define a function called
foo:
1 function foo() {
2 alert("called foo");
3 }
If we want
foo to be called when the mouse is moved over a link, we need to have
foo called during the
onmouseover
event of the link. This is called subscribing
foo to the
onmouseover event. This can be done in the link tag itself:
1 <a id="ATestLink" href="#" onmouseover="foo();">Test MouseOver</a>
Or, we can do this programmatically within JavaScript:
1 document.getElementById("ATestLink").onmouseover = foo;
Note the lack of parenthesis after
foo. This is because we want to reference the function here, not call it to execute.
Articles and downloads sponsored by:
Thanks! Amazon commissions help me pay for textbooks.
Basic event handlers, as shown above, have two limitations:
- The functions called by an event handler may have parameters that we need to pass to them, but we have no way to pass these.
- Only one event handler can subscribe to an event, and event handlers in different scripts may therefore overwrite each other.
We can solve both of these issues with closures. Closures are nothing more than functions defined within other functions.
When a function is defined within another function, the created function has access to the variables in scope within the containing function.
Even after the containing function ends, these variables remain accessible to the created function.
As an example, let's look at the following code:
1 function TryClosures() {
2 var message = "This will be displayed";
3 document.getElementById("ATestLink").onmouseover = function() { alert(message); };
4 }
This function creates a variable called message, within the scope of
TryClosures. Then, it creates a function and assigns this new function
to the
onmouseover event of a link. Because the function was created within
TryClosures, it has access to the
message
variable created within
TryClosures, even after
TryClosures ends.
This solves the first problem, by allowing us to pass local variables as parameters to event handler functions. In this case, we passed the local variable
message as a parameter to
alert. Note that, if we change the value of
message after creating the closure,
the
alert will show the new value.
Our second problem is that we can currently assign only one event handler to each event. For example, in the following code, only
foo2 would
be called during the
onmouseover event, because the assignment of
foo2 overwrites the assignment of
foo1.
1 document.getElementById("ATestLink").onmouseover = foo1;
2 document.getElementById("ATestLink").onmouseover = foo2;
We can solve this by using a closure to remember the previous value of the event handler, and call it in addition to the new value. We can generalize this in a
function called
AddEventHandler:
1 function AddEventHandler(e, x) {
2 return function() { if(e) e(); x(); };
3 }
AddEventHandler is used like this:
1 var testLink = document.getElementById("ATestLink");
2 testLink.onmouseover = AddEventHandler(testLink.onmouseover, foo1);
3 testLink.onmouseover = AddEventHandler(testLink.onmouseover, foo2);
4 testLink.onmouseover = AddEventHandler(testLink.onmouseover, foo3);
In this example, when the mouse moves over the test link,
foo1,
foo2, and
foo3 are all called. This works
because
AddEventHandler gets the current function assigned to the event as the parameter
e. The function returned
by
AddEventHandler calls both
e, the original event, and
x, the added function.
AddEventHandler simply returns a new function
that calls the existing and added functions. Using this function ensures that you don't overwrite any previously assigned event handlers, causing a
frustrating and hard-to-find bug.
The above functions expand the functionality of JavaScript events, but there are some browser issues to overcome as well. To illustrate the
primary issue, let's look at a specific instance:
In Internet Explorer, when a user moves the cursor over a link, the
onmouseover event of the link is fired. At the same time,
the link text may change to it's hover style (usually a slight change in color). Internet Explorer, at this time, often fires the
onmouseout
event, immediately followed by the
onmouseover event again. Internally, the browser is thinking that you moved over the link, but then
moused off of the original link and moused over the hover-styled version of the link.
This can be a common scenario with events. The browser may rapidly fluctuate between opposite events before settling on the actual event.
In addition, rapid mouse movement, key presses, and other user actions may cause a rapid deluge of legitimate events.
If you try to handle all of these events, they are handled at almost exactly the same time, occasionally overwriting each other and using many times
the processor power. In the end, your script can end up in an unexpected state, and the page performance can suffer.
To correct this, we may want to make sure we handle only the last event fired within a rapid series of events. This way, we wait for the page state to
settle before we update the state of our script. We can accomplish this by holding all events for
a short amount of time, and canceling pending events if a contradictory or superseding event occurs within the delay.
To start, let's see the JavaScript for a simple event handler for the
onmouseover and
onmouseout events of a link. After we see the
JavaScript for a standard solution, we'll look at how we can trivially alter this code to implement delays.
1 function HandleMouseOver() {
2 alert("handled mouseover");
3 }
4
5 function HandleMouseOut() {
6 alert("handled mouseout");
7 }
8
9 var testLink = document.getElementById("ATestLink");
10 testLink.onmouseover = AddEventHandler(testLink.onmouseover, HandleMouseOver);
11 testLink.onmouseout = AddEventHandler(testLink.onmouseout, HandleMouseOut);
We want to add a delay to insulate
HandleMouseOver and
HandleMouseOut from bad event calls. So we simply add a set of
additional functions to handle this delay. Here is the revised code:
1 var mouseEventTimer;
2
3 function HandleMouseOver() {
4 alert("handled mouseover");
5 }
6
7 function HandleMouseOut() {
8 alert("handled mouseout");
9 }
10
11 function HandleMouseOverAfterDelay() {
12 clearTimeout(mouseEventTimer);
13 mouseEventTimer = setTimeout(function() { HandleMouseOver(); }, 150);
14 }
15
16 function HandleMouseOutAfterDelay() {
17 clearTimeout(mouseEventTimer);
18 mouseEventTimer = setTimeout(function() { HandleMouseOut(); }, 150);
19 }
20
21 var testLink = document.getElementById("ATestLink");
22 testLink.onmouseover = AddEventHandler(testLink.onmouseover, HandleMouseOverAfterDelay);
23 testLink.onmouseout = AddEventHandler(testLink.onmouseout, HandleMouseOutAfterDelay);
Any event handler for the
onmouseover and
onmouseout events that wants to run has to first survive a 150 millisecond
delay. If the event is superseded by another
onmouseover or
onmouseout event within 150 milliseconds, it is canceled,
but if the event holds uncontested for the entire delay, it continues. 150 milliseconds is still an extremely quick response time, so we don't hold
up the user noticeably. And, since the delay is so short, we can assume that any events that occur together within this timeout likely fall into the
buggy category described above and can be ignored.
This handling is annoying because it adds another layer of code that must be written and maintained. However, it makes scripts more robust and
can improve performance and reliability, so i generally consider it a best-practice to insulate events this way. This is especially true for mouse and
key events, because of how rapidly users can move the mouse and press keys on a page.
Note that the
setTimeout calls in this new code also use closures. This allows us to pass parameters through the
setTimeout calls.
So, if there were parameters to
HandleMouseOver, we could give
HandleMouseOverAfterDelay the same parameters, and pass the
parameters through the closure.
Altogether, this article has presented three best-practices techniques for JavaScript event handlers:
- Use
AddEventHandler to add event handlers, instead of directly assigning event handlers and overwriting earlier assignments.
- Use closures to call event handlers when you need to pass parameters.
- Insulate event handlers behind a delay, so that calls can be cancelled if the event is superseded by another rapid firing of the same or a contradictory event.
Comments are appreciated. If there are any additional topics you would like information on, or other best-practices that I missed, feel free to let
me know and I will expand this article.