Making sense of async.js
async.js is a fascinating library for taming asynchronous javascript. It’s highly experimental, and as far as I know, it only works in firefox. But the idea is an important (and useful) one, so I think it’s definitely worth knowing about.
There are a few approaches to dealing with asynchronous javascript:
-
Compile [some language] to javascript This is the approach taken by GWT, pyjamas, and many others. It’s usually extremely heavyweight, so it mainly makes sense for big apps.
-
Compile [almost-javascript] to javascript Most notably this includes Narrative Javascript and its successor, Strands. This is a reasonable approach, but the lack of maturity / tool support makes debugging extremely hard. Also, I ran into a number of bugs in both these libraries. The thought of finding and fixing more of those bugs is not at all fun.
-
Write a javascript library This is doable, but typically looks hideous, convoluted, and is usually quite burdensome to try and use.
Async.js is the most plausible attempt at #3 that I’ve seen. It uses a reasonably clever (but not unknown) trick to to turn Javascript 1.7’s generator functionality into an event-based coroutine system. Specifically, this allows for a program to “wait” for a callback, while not actually blocking the javascript interpreter (as a synchronous AJAX call would). Go read the async.js page to learn more, because the following isn’t going to make much sense if you don’t know roughly how to use async.js.
The divide between functions and async-aware functions
Using the default “function call descriptor” (henceforth referred to as FCD) provided by the to
function, your functions are required to take precisely two arguments - a list of regular arguments, and a callback function. This is awkward, as it is not at all like typical javascript functions.
Passing in a function as an argument to the to
function allows for a more natural use, where you simply provide one less than the number of arguments expected, and the FCD will fill in that last argument with the appropriate callback function. I think this use is much more natural, because it allows you to use async’s functionality to wait for functions that know nothing of async.js.
Still, I think more can be done. With the current implementation, utilizing async.js still means you have to:
- use the
yield
keyword - wrap your function in a call to
to
- provide the arguments not to your function, but to the result of the
to
call
The yield
requirement can’t be removed without a preprocessor, but I think that for most cases, that should be all you need. Essentially, when you yield to a FCD using to(func)(arg1, arg2, arg3)
, you need to be calling a function that expects a callback object as its 4th (and final) argument. I think that providing one less argument than required is enough to indicate that you are trying to generate a FCD. Therefore, I have written a wrapper function (called baked
) that checks the number of arguments provided against the number of arguments the function it wraps would normally expect. If in this example you were to provide all four arguments, the wrapped function would be called normally. If, however, you only provide three arguments, the wrapper will return an FCD.
That is:
yield func(a,b,c);
would yield a FCD for async.js to deal with, while:
func(a,b,c, function() { /* do whatever... */ }
would call the function normally.
This allows you to call a function directly if you are providing your own callback, and all you need to do when using async.js is to use the yield
keyword, and omit the callback argument (which is easy enough, since you likely won’t have a callback function to give it).
Using it
If this is the kind of thing that interests you, please check out my js-bakery project on github (specifically, async-bakery.js). The basic usage is that every function participating in async.js behaviour (either by yielding, or by calling a function that will itself yield) must be “baked”. You can do this on a function-level by using myFunction.*bake()*
, or you can create a constructor function that will automatically bake all of its methods by using MyConstructor.BakedConstructor()
.
It should probably be noted that this system will only work reliably for functions with fixed-length arguments. Any function that takes a variable number of arguments will still need to be wrapped in a call to to
in order to force a FCD to be generated. Thankfully, functions that take variable-length arguments and a callback are few and far-between.
An example for my impatient friends…
The following example is a fairly complete depiction of how you might use baking with async.js. This assumes only that your page includes both async.js and async-bakery.js.