Here it is, the second in a continuing series on browser-based JS development.
Last time I lightly covered the basics of the Page event life cycle, with onload and onunload being the two major players. However, I didn’t go into what events are and how they impact how you develop a web app. To get where we’re going, we really have to go back to the early days of Windows((The following statements may be equally applicable to X, NEXT, Cocoa, Carbon, GTK, whatever. I don’t really know. I’ve never done serious development on those toolkits. Further enlightenment would be appreciated if anyone does have experience.)).
If you’ve ever done Windows GUI programming, you’re probably familiar with the MessagePump (or DoEvents in VB). In classic Windows GUI programming, there is one thread, and only one thread, responsible for drawing and updating the UI. By default, all of your code runs on this same thread. At the heart of this thread lives a MessagePump, which is responsible for pulling a message off a queue, cracking it to determine what it’s all about, and then calling the parties interested in said message. To do things in the application, you tell the application that you’re interested in certain messages, like say WM_CLOSE, and the message pump happily calls your function when that message comes through the queue. JavaScript + DOM works in a very similar way.
After you write your first bit of DOM script, you probably notice that there’s no real main entry point for the page; no int main() {} or the like. Code that lives in a script block is run when the page loads, and you set up event listeners to be called when interesting things happen, like say a button is clicked. There are a number of ways to connect your function to an event; I’ll cover those in another one of these essays, but suffice to say for now, if you want to listen for an event, use dojo.connect(). For example, to listen for a click on a button you’d say dojo.connect(“myButton”, “click”, function(evt) { alert(‘myButton was clicked’); }); More on this later.
So far you have two ways to get your code called. 1) Code in script blocks is run while the page is being initialized and 2) in response to some DOM event. There’s a third: you can ask that some code be run at a set point in the future using setTimeout or setInterval. It looks something like
setTimeout(function() { alert('5 seconds have elapsed'); }, 5000)
or
setInterval(function() { alert('i run every 5 seconds'); }, 5000);
Both of these return a handle that can be later passed to clearTimeout or clearInterval to kill the pending timer. So something like
var myHandle = setTimeout(function(){}, 5000);
clearTimeout(myHandle);
would prevent the function from being called at all. It’s important to understand that the time given to these functions is more like a suggestion than a strict contract. Guess why…
All of your JavaScript runs on the same thread. Period. This thread is also responsible for updating the GUI, so it’s fairly important to limit what you’re doing at any given time. If you’re doing something crazy, like say bubble-sorting a million element array, you’re blocking everything else. The browsers do try to prevent you from going buck-wild; I would guess you’ve all seen the “Holy crap this script is going crazy!” dialog from IE or Firefox at some point. The single-threaded nature of the DOM also explains why the time passed to setTimeout and setInterval is merely a suggestion: the browser will run your code when it can, on the GUI thread.
Sometimes you just need to do something that takes a while. So how do you break it up? Here’s where closures come in really handy, along with the run-when-you-can nature of setTimeout ((here’s a handy little wrapper that better expresses the intent of setTimeout(func, 0):
function defer(func){
return setTimeout(func, 0);
}
usage: defer(function() { /* work work work */ });
I don’t generally use it because it’s just one more function call to get stuff done and setTimeout(func, 0) is fairly easy to read anyway.)). Consider the following:
function countToAMillion(callback) {
var currentState = 0;
var iters = 1000;
var goal = 1000000;
function worker() {
var i = iters;
while(currentState < goal && i !== 0) {
currentState++;
i--;
}
if(currentState === goal) {
callback();
} else {
setTimeout(arguments.callee, 0);
}
}
setTimeout(worker, 0);
}
This function counts to a million, yielding to the browser every 1000 iterations ((see the Mozilla docs for more details on arguments.callee)). By yielding, you’re giving the browser a chance to catch up, dispatch other events that need to happen, and then resume processing your workload.
In general, it’s a good idea to keep your code short and sweet, especially when responding to events. A good pattern to follow looks something like this (though we don’t use it a lot today):
- inspect event to see if you can handle it
- propagate or cancel the event as needed
- defer work you need to do in response to the event
or in code:
Yes in this case countToAMillion instantly defers it’s own work, but I don’t necessarily know that when I’m wiring up the event handler. Also, sometimes you have to do a little work to figure out if you can actually handle the event; that’s fine, just try to keep it quick.
This one got a little long, but I hope it’s a good intro on how to construct an app and how to best respond to events. To sum up: everything runs on one thread so yield when you can to give your neighbors time to do their work too. Questions, comments, snide remarks welcome. Next time, events and topics.