Update: someone has come up with a slightly different solution to the problem discussed (See here)
Meanwhile...
... I mean, what other sort of experience would one have with Asynchronous Javascript And Xml?
Indeed, there's a lot to like about a technique that allows you to update your webpage with a snippet of information retrieved from the server *without* having to reload the whole sorry spectacle. It allows for much greater range of interaction with the user, as well. As I type this blog entry, a small message at the foot of the page is reassuring me that my draft is being stored automatically. How? Presumably a little java applet is being fired at regular intervals, which invokes an XMLhttpRequest (or XMLObject, if you're using IE 6 or earlier), and sends the current data back to the server.
Well, having eulogized the technique, what have I found to grouch about?
The ProblemAs an old song put it: 'nice legs, shame about the face!'. One thing, in particular, has been annoying me about the callback mechanism. On just about every tutorial on the subject (and since there are plenty, rather than reinvent the wheel here, I suggest you go google 'Ajax tutorial'! ) you will see something like the following code:
var ajaxHandler = false;
onClick (element)
{
try
{
// Firefox, Opera 8.0+, Safari
ajaxHandler=new XMLHttpRequest();
}
catch (e)
{
// Internet Explorer
try
{
ajaxHandler=new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e)
{
try
{
ajaxHandler=new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e)
{
alert("Your browser does not support AJAX!");
}
}
}
if (ajaxHandler)
{
ajaxHandler.open("GET", "http://www.etc.etc/myurl", true);
ajaxHandler.onreadystatechange =
function()
{
if (ajaxHandler.readyState == 4)
{
// Handle the reponse!
}
}
ajaxHandler.send(null);
}
}
Perfectly workable as is, and quite adequate for a simple starting tutorial on how to make an AJAX call.
Except...
Except, how does the callback function know where to retrieve the results? It doesn't get provided as a call parameter. Nope!
The callback function has to go grubbing around for a global variable! This is ugly, and I don't just mean in the cosmetic sense either.
It Gets WorseAs you gain confidence with javascript and Ajax, you will start to use the 'asynchronous' part of the technique more and more: making several Ajax calls simultaneously. Possibly making several calls to the same Ajax function simultaneously... using the same global object! As anyone familiar with multiple processing will attest, this situation is not just ugly, but downright bad.
While Quirksmode has an interesting account of what can happen, and how to avoid the worst of it, there is an astonishing paucity of advice on this topic. So, I had a bit of a tinker, and here is my solution.
Preliminary Tidying UpFirst of all, I like a place for everything, and everything in its place. Just as a server process might create a short lived thread/process to handle each request it receives, I would like to see each Ajax call result handled by a single object.
If XMlhttpRequest passed itself to the call back function, we would have a solution (and I wouldn't have a topic). Can we simulate this?
Of course we can, by re-writing the above listing as a wrapper function that implements a chain of command. The main routine then becomes:
function MyCallback (handler)
{
if (handler.readyState == 4)
{
// Prettier, but!
}
}
function onClick (element)
{
CallAjax (MyCallback, "http://www.etc.etc/myurl");
}
This separates the code handling AJAX, which now reads as follows.
var ajaxHandler = AjaxHandler ();
function AjaxHandler ()
{
try
{
// Firefox, Opera 8.0+, Safari
handler=new XMLHttpRequest();
}
catch (e)
{
// Internet Explorer
try
{
handler=new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e)
{
try
{
handler=new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e)
{
alert("Your browser does not support AJAX!");
}
}
}
return handler;
}
function CallAjax (callback, url)
{
if (ajaxHandler)
{
ajaxHandler.open("GET", url, true);
ajaxHandler.onreadystatechange =
function()
{
callback (ajaxHandler);
}
ajaxHandler.send(null);
}
}
An Obvious Solution...Well, this might look a bit tidier but, apart from that, nothing much has been achieved: while the callback function proper now has a handler provided to it, it's still going to be the same handler each time a call is made. We want to create a request object each time we make a call.
So, would this do?
:
function CallAjax (callback, url)
{
var ajaxHandler = AjaxHandler();if (ajaxHandler)
{
ajaxHandler.open("GET", url, true);
ajaxHandler.onreadystatechange =
function()
{
callback (ajaxHandler);
}
ajaxHandler.send(null);
}
}
...That Doesn't Work!Unfortunately, no. The problem here is that, although a new handler object is being created, with its own callback object, the language is java
script, ie the command is interpreted, so the dummy function is referring to whatever the handler is at the time the callback was received, which probably isn't what you expected!
A Refinement...However, there is a way around this: retain the global handler as an array, and refer to the required handler by its array index:
var ajaxHandler = new Array();function AjaxHandler ()
{
:
var handle = ajaxHandler.length;
ajaxHandler[handle] = handler;
return handle;
}
function CallAjax (callback, url)
{
var handle = AjaxHandler();
if (handle >= 0){
var handler.open("GET", url, true);handler.onreadystatechange =
function()
{
callback (ajaxHandler[handle]);
}
handler.send(null);
}
}
Now we're beginning to cook! Each new request handler is stored in a static array, and the dummy callback function refers to it by its index. This is important because to refer to a handler specifically will cause the same problem as before: several callback functions referring to an indeterminate handler.
Why this is so has something to do with the way the dummy function is generated (it is a class) . To refer to the handler object itself is to refer to it by reference (ie the address of the temporary script variable) to refer to the array index is to refer to it by value (an integer). The latter approach ensures that each callback method refers to a specific handler.
CommentaryI haven't been using it long enough to give a definitive judgement but, so far, this technique seems to work well. I do have some qualms about that array of request objects growing in the corner like a stack of dirty dishes. It should get cleared away each time the page is refreshed. However, it may cause memory problems if the page persists for long. I am loath to suggest removing old objects from the array since it would upset the index references of current Ajax calls. If this is a concern, you could replace each object entry with an integer as it completes.
An EmbellishmentOne final embellishment to the procedure. Since we now have a single definition of a callback function, we can use it to perform useful universal tasks such as telling whether or not the server side script ran properly. eg, when returning the result as XML, this is readily done by checking to see whether or not the responseXML field was generated. If it wasn't, then the raw text may contain the warning diagnostics. Similar results can be achieved by searching the result Text field for error indicators.
function CallAjax (callback, url)
{
var handle = AjaxHandler();if (handle >= 0)
{
var handler.open("GET", url, true);
handler.onreadystatechange =
function()
{
var response = ajaxHandler[handle];
if (response.responseXML)
{callback (response );
}
else
{alert (response.responseText);
}
}
handler.send(null);
}
}
OK? Now go and play.
Final Listing
The following is the complete final listing:
var ajaxHandler = new Array();
function AjaxHandler ()
{
try
{
// Firefox, Opera 8.0+, Safari
handler=new XMLHttpRequest();
}
catch (e)
{
// Internet Explorer
try
{
handler=new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e)
{
try
{
handler=new ActiveXObject("Microsoft.XMLHTTP");
}
catch (e)
{
alert("Your browser does not support AJAX!");
}
}
}
var handle = ajaxHandler.length;
ajaxHandler[handle] = handler;
return handle;
}
function CallAjax (callback, url)
{
var handle = AjaxHandler();
if (handle >= 0)
{
var handler = ajaxHandler[handle];
handler.open("GET", url, true);
handler.onreadystatechange =
function()
{
var response= ajaxHandler[handle];
if (response.responseXML)
{
callback (response);
}
else
{
alert (response.responseText);
}
}
handler.send(null);
}
}