Overcoming Asynchronous Anxiety By Testing JavaScript's Event Loop
JavaScript runs on an event loop. I know this. I've tested this before. I understand race conditions and intervals. And yet, I still find myself harboring anxiety when it comes to asynchronous JavaScript actions in the browser. My latest unfounded and irrational fear comes from monitoring the location Hash of the browser. Specifically, keeping the location hash in sync with an internal hash representation.
When I build a JavaScript application that reacts to changes in the window location's Hash value, I keep an internal representation of the hash that I use as a control against which to compare the ongoing location hash. The monitoring of the location hash takes place in some sort of interval; and, when the location hash and the internal hash fall out of sync, I know that my location hash has changed. And, I can react to that change as necessary.
If I want to update the location hash manually - that is, I want my application to manipulate the location hash - I'll usually do something like this:
- Turn OFF location hash monitoring.
- Set internal hash.
- Set location hash.
- Turn ON location hash monitoring.
I turn off the location hash monitoring while updating the internal hash because I am worried that my monitoring interval will catch me in between steps 2 and 3: where I have updated the internal hash, but have not yet updated the location hash. I am worried that this will inaccurately detect a change in the location hash.
This fear is irrational.
This scenario does not exist.
The JavaScript event loop will not allow it.
I know this rationally; but, as I just said, the fear is irrational. To try and cure myself of the fear, I put together a quick demo that tries to put my fear to the test. In the following code, we're going to monitor the location hash. And, we're going to update the internal hash and the location has without pausing the monitor. And, we're going to try to waste a lot of time in between those two steps.
<!DOCTYPE html>
<html>
<head>
<title>Understanding JavaScript Event Loops And Timeouts</title>
</head>
<body>
<h1>
Understanding JavaScript Event Loops And Timeouts
</h1>
<p>
<a href="#" class="hashTrigger">Change Hash</a>
</p>
<p class="text">
This is just some text in a DOM element that I will be
manipulating in order to provide a process-heavy way
of killing some time. Yeahhhhh booooyyyyyyy!
</p>
<!-- Initialize scripts. -->
<script type="text/javascript" src="../jquery-1.7.1.js"></script>
<script type="text/javascript">
// Get and cache our DOM elements.
var dom = {
trigger: $( "a.hashTrigger" ),
text: $( "p.text" )
};
// Keep track of our current hash value.
var currentHash = location.hash;
// I update the internal hash and the window hash. In between
// those two updates, however, I kill time to see if the
// browser's event loop will notice the discrepency.
function setHash( newHash ){
// First, we'll set the internal hash value so that
// when the location hash changes, it changes INTO
// sync with the internal hash (not OUT OF sync).
currentHash = ("#" + newHash);
// Waste time updating the DOM. We're doing this to see
// if our interval for location-checking will see the
// internal hash change before we change the window's
// hash value.
wasteTime();
// Update the location.
location.hash = newHash;
};
// I check to see if the window hash and the internal hash
// are out of sync.
function checkHash(){
// Make sure they match.
if (currentHash !== location.hash){
console.log(
"Hash out of sync:",
currentHash,
location.hash
);
}
}
// I try to waste time by updating the DOM and causing
// re-rending of the page.
function wasteTime(){
// Increase font size.
for (var i = 1 ; i < 10000 ; i++){
dom.text.css( "font-size", (i + "px") );
}
// Decreate font-size.
for (var i = 10000 ; i > 20 ; i--){
dom.text.css( "font-size", (i + "px") );
}
}
// Set up the hash trigger to always change the hash to the
// current timestamp.
dom.trigger.click(
function( event ){
// Kill the default event - this isn't a real link.
event.preventDefault();
// Set the hash to the epoch time.
setHash( (new Date()).getTime() );
}
);
// Start monitoring the hash change.
setInterval( checkHash, 10 );
</script>
</body>
</html>
If you look in my setHash() function, you'll see that it calls the method, wasteTime(), in between setting the internal hash representation and changing the location hash. This wasteTime() method runs a bunch of process-intense DOM (Document Object Model) updates that cause the browser to redraw (the slowest thing you can do in JavaScript). And still, even with this very noticeable delay in processing, the location monitoring never finds the location hash out of sync.
NOTE: You can see this in the video.
This is because JavaScript's event loop won't execute the next tick of the monitor interval until all the synchronous processes of the current tick are done. This means that even if my current tick is doing a lot of work, including updating both hashes (internal and external), it will successfully complete before the monitor interval is allowed to execute.
Hopefully this exploration will be enough immersion therapy to put my event loop anxiety to rest.
Want to use code from this post? Check out the license.
Reader Comments
That was a short, but highly informative video. In fact, I've often steered clear of setInterval and setTimeout for just this reason. So if I can extend the argument logically, does this mean that setInterval waits for its bound function to be the only function on the call stack before executing the next iteration?
great post, this should help those of us who suffer irrational eventusansaphobia (fear of the event loop). personally, just knowing that the complete execution of the handler takes precedence over the clock puts my mind almost 100% at ease. almost.
@Mark,
I can't say that I know how things get chosen to run; but, what I can say is that when the callback in setTimeout() / setInterval() is running, you can be *sure* that it's not conflicting with code outside of the callback.
@Michael,
Thanks! Hoping to put all of our minds at ease :D