When we want to make a piece of code run in the future in JavaScript, we put it in a callback. A callback is just an ordinary function, except that it’s passed to a function like setTimeout or bound as a property like document.onready. When a callback runs, we say that an event (e.g., the timeout elapsing or the document becoming ready) has fired.
Of course, the devil is in the details, even for something as seemingly simple as setTimeout. A common description of setTimeout goes something like this:
Given a callback and a delay of n milliseconds, setTimeout runs that callback n milliseconds later.
But as we’ll see in this section, and throughout this chapter, that description is seriously flawed. In most cases, it’s only approximately true. In others, it’s flat-out wrong. To truly understand setTimeout, we have to understand the JavaScript event model as a whole.
To begin our exploration of setTimeout, let’s look at a simple example of a situation that often mystifies new JavaScripters, especially those coming from multithreaded languages like Java and Ruby.
EventModel/loopWithTimeout.js | |
| for (var i = 1; i <= 3; i++) { |
| setTimeout(function(){ console.log(i); }, 0); |
| }; |
<= | 4 |
| 4 |
| 4 |
Most newcomers to the language would expect the loop to produce the output 1, 2, 3, or perhaps a juxtaposition of those three numbers as the three timeouts (each scheduled to go off in 0 milliseconds) race to fire first.
To understand why the output is 4, 4, 4 instead, there are three things you need to know.
There’s only one variable named i, scoped by the declaration var i (which, incidentally, scopes it not within the loop but within the closest function containing the loop).
After the loop, i === 4, having been incremented until it failed the condition i <= 3.
JavaScript event handlers don’t run until the thread is free.
The first two concepts are in the realm of JavaScript 101, but the third comes as more of a surprise. When I first started using JavaScript, I didn’t quite believe it. Java had trained me to fear that my code could be interrupted at any moment. A million potential edge cases filled me with anxiety as I wondered, “What if a rare event happened between these two lines of code?”
And then one day, that burden was lifted from me….
This piece of code demolished my preconceptions about JavaScript events:
EventModel/loopBlockingTimeout.js | |
| var start = new Date; |
| setTimeout(function(){ |
| var end = new Date; |
| console.log('Time elapsed:', end - start, 'ms'); |
| }, 500); |
| while (new Date - start < 1000) {}; |
In my multithreaded mind-set, I’d expected only 500ms to go by before the timed function ran. But that would have required the loop, designed to last a full second, to be interrupted. Instead, if you run the code, you’ll get something like this:
<= | Time elapsed: 1002ms |
You’ll probably get a slightly different number; setTimeout and setInterval are, alas, a lot less precise than you’d hope (see Timing Functions). But it will definitely be at least 1000, because the setTimeout callback can’t fire until the while loop has finished running.
So, if setTimeout isn’t using another thread, then what is it doing?
When we call setTimeout, a timeout event is queued. Then execution continues: the line after the setTimeout call runs, and then the line after that, and so on, until there are no lines left. Only then does the JavaScript virtual machine ask, “What’s on the queue?”
If there’s at least one event on the queue that’s eligible to “fire” (like a 500ms timeout that was set 1000ms ago), the VM will pick one and call its handler (e.g., the function we passed in to setTimeout). When the handler returns, we go back to the queue.
Input events work the same way: when a user clicks a DOM element with a click handler attached, a click event is queued. But the handler won’t be executed until all currently running code has finished (and, potentially, until after other events have had their turn). That’s why web pages that use JavaScript imprudently tend to become unresponsive.
You might sometimes hear the term event loop used to describe how the queue works. It’s as if your code is being run from a loop that looks like this:
| runYourScript(); |
| while (atLeastOneEventIsQueued) { |
| fireNextQueuedEvent(); |
| }; |
One implication of this is that each event that fires will be at the root of the stack trace. We’ll learn more about that in Handling Async Errors.
The ease of event scheduling in JavaScript is one of the language’s most powerful features. Async functions like setTimeout make delayed execution simple, without spawning threads. JavaScript code can never be interrupted, because events can be queued only while code is running; they can’t fire until it’s done.
In the next section, we’ll take a closer look at the building blocks of async JavaScript.
3.144.172.233