JavaScript: Avoiding the Race Condition with Ajax
The main problem with the code we presented earlier is that it didn't take into account the possibility of multiple XMLHttpRequests running concurrently and therefore falling vicitim to the race condition which is a common occurence in asynchronous systems.
The problem is that the single global variable req is overwritten as soon as another call is made to the loadXMLDoc() function, effectively terminating any previous calls that have not yet reached the execution state (the processReqChange() function).
Introducing the AjaxRequest class
It's actually a trivial exercise to transform the previous code to use a class and no longer rely on global variables. The modified code is shown here:
function AjaxRequest()
{
// Original JavaScript code by Chirp Internet: www.chirpinternet.eu
// Please acknowledge use of this code by including this header.
var req;
var method = "GET";
var nocache = false;
this.loadXMLDoc = function(url, params)
{
if(window.XMLHttpRequest) {
try {
req = new XMLHttpRequest();
} catch(e) {
req = false;
}
} else if(window.ActiveXObject) {
try {
req = new ActiveXObject("Msxml2.XMLHTTP");
} catch(e) {
try {
req = new ActiveXObject("Microsoft.XMLHTTP");
} catch(e) {
req = false;
}
}
}
if(req) {
req.onreadystatechange = processReqChange;
if(nocache) {
params += (params != '') ? '&' + (new Date()).getTime() : (new Date()).getTime();
}
if(method == "POST") {
req.open("POST", url, true);
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
req.send(params);
} else {
req.open(method, url + '?' + params, true);
req.send(null);
}
return true;
}
return false;
}
this.setMethod = function(newmethod) { method = newmethod.toUpperCase(); }
this.nocache = function() { nocache = true; }
// define private methods
var getNodeValue = function(parent, tagName)
{
var node = parent.getElementsByTagName(tagName)[0];
return (node && node.firstChild) ? node.firstChild.nodeValue : '';
}
var processReqChange = function()
{
if(req.readyState == 4 && req.status == 200) {
var response = req.responseXML.documentElement;
var commands = response.getElementsByTagName('command');
for(var i=0; i < commands.length; i++) {
method = commands[i].getAttribute('method');
switch(method) {
case 'alert':
var message = getNodeValue(commands[i], 'message');
window.alert(message);
break;
case 'setvalue':
var target = getNodeValue(commands[i], 'target');
var value = getNodeValue(commands[i], 'value');
if(target && value != null) {
document.getElementById(target).value = value;
}
break;
case 'setdefault':
var target = getNodeValue(commands[i], 'target');
if(target) {
document.getElementById(target).value = document.getElementById(target).defaultValue;
}
break;
case 'focus':
var target = getNodeValue(commands[i], 'target');
if(target) {
document.getElementById(target).focus();
}
break;
case 'setcontent':
var target = getNodeValue(commands[i], 'target');
var content = getNodeValue(commands[i], 'content');
if(target && content != null) {
document.getElementById(target).innerHTML = content;
}
break;
case 'setstyle':
var target = getNodeValue(commands[i], 'target');
var property = getNodeValue(commands[i], 'property');
var value = getNodeValue(commands[i], 'value');
if(target && property && value) {
document.getElementById(target).style[property] = value;
}
break;
case 'setproperty':
var target = getNodeValue(commands[i], 'target');
var property = getNodeValue(commands[i], 'property');
var value = getNodeValue(commands[i], 'value');
if(value == "true") value = true;
if(value == "false") value = false;
if(target) {
document.getElementById(target)[property] = value;
}
break;
default:
window.console.log("Error: unrecognised method '" + method + "' in processReqChange()");
}
}
}
}
}
You can download this class as ajaxrequest.js.
How does this affect my code?
Old Version
With the old code you would call the loadXMLDoc() function directly:
<script src="xmlhttp.js"></script>
<script>
var callAjax = function(method, value, target) {
var params = "method=" + method + "&value=" + encodeURIComponent(value) + "&target=" + target;
loadXMLDoc("validate.php", params);
return true;
};
</script>
New Version
With the new code you first need to instantiate an AjaxRequest object and then call it's loadXMLDoc() function. The relevant changes are highlighted in the code below:
<script src="ajaxrequest.js"></script>
<script>
var callAjax = function(method, value, target) {
var req = new AjaxRequest();
var params = "method=" + method + "&value=" + encodeURIComponent(value) + "&target=" + target;
req.loadXMLDoc("validate.php", params);
return true;
};
</script>
The old code will still work perfectly for simple applications where only one request can be made at a time, but in more complex systems - relating to form validation for example - you should definately be using the AjaxRequest class instead.
Extra Functionality
Two additions to the code that you might want to know about are the ability to set the request method to POST (or HEAD or any other valid method) and the ability to disable caching as described in the previous article.
To set the request method to POST for example:
var req = new AjaxRequest();
req.setMethod("POST");
req.loadXMLDoc(url, params);
And to disable caching for GET requests:
var req = new AjaxRequest();
req.nocache();
req.loadXMLDoc(url, params);
If you convert the call from GET to POST don't forget to also change the server-side script to accept POST variables.
Hiding from older browsers
Older browsers such as MSIE 5.0 do not support encodeURIComponent() and will not be able to run the callAjax() function presented above. To get around this you can either:
- replace encodeURIComponent() with the JavaScript escape function; or
- check first that encodeURIComponent() is supported before using it.
Implementing the first option means that you will no longer be able to safely pass UNICODE characters to the server-side script. This could be a problem if you're passing user input rather than just simple variables.
The second option involves a slight change to the function:
<script>
var callAjax = function(method, value, target) {
if(encodeURIComponent) {
var req = new AjaxRequest();
var params = "method=" + method + "&value=" + encodeURIComponent(value) + "&target=" + target;
req.loadXMLDoc("validate.php", params);
}
return true;
};
</script>
Having the condition added around the Ajax code means that it will not be executed in MSIE 5.0 or equivalent browsers that don't support encodeURIComponent().
Related Articles - Ajax
- JavaScript Making an asynchronous request before following an href
- JavaScript Form Validation using Ajax
- JavaScript Using a Promise to make sequential Ajax requests
- JavaScript Avoiding the Race Condition with Ajax
- JavaScript Using XMLHttpRequest to log JavaScript errors
- JavaScript Making a HEAD request via an Ajax script
- JavaScript Recording Outbound Links using Ajax
- JavaScript Making sure form values are unique using Ajax
- PHP Generating an XML Response for Ajax Applications
- PHP Ajax script for making cURL HEAD requests
- JavaScript Web Services using XMLHttpRequest (Ajax)
Xavi 1 August, 2012
I have made a big profit of these articles about using AJAX with XML. I was looking for this solution without having any idea that I had to use XML.
Until having read this article also I didn't have any idea of the need of making a class to send many AJAX requests. I solved that on my own with a very simple solution: Using the "req" variable as an array element and adding a numeric parameter on the loadXMLDoc function, wich makes req[number] inside the function and creates as different unique AJAX requests as I set with different numbers.
Anyway, using Objects seems to me much more ellegant so I'll try to implement it on my actual functions. Thank you for these great articles!
Jeff 20 January, 2009
Thanks for the script. I noticed that it pops an error message in IE 6. I was able to fix it by changing this line: if(!req.responseXML) return;
to this: if(!req.responseXML || req.responseXML == 'null' || req.responseXML == 'undefined') return;
I'm not sure if this is the cleanest solution but it fixed the javascript error.
Thanks for that Jeff. I don't always have time to test everything properly in Windows - it give me a headache
Ian 23 May, 2006
Just trying out your Ajax approach for a new site using ASP, but I can't get it working in Firefox on Windows...
... ignore my last message - i figured it out - i used:
response.ContentType = "text/xml"
instead of:
response.AddHeader "Content-Type","text/xml"
and it worked.
Thanks for a great script!