...

View Full Version : Smooth async requests



AndrewGSW
12-17-2012, 09:59 PM
When requesting a sequence of data asynchronously using ajax the data is not returned at regular intervals, or necessarily in the order the requests were made. The following code uses setInterval to update the page at regular intervals - creating a smooth page-update. The data is inserted into table cells according to the order the data was received.

The setIntervals are not initiated until half the data has been received, and will be cleared when all data has been processed. (An else clause could be added so that the setInterval is cleared if there is an error in retrieving the remaining data.)

asyncAjax.html

<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<style type="text/css">
#theTable { outline: 1px solid gray; }
#theTable td {
width: 40px; height: 50px;
}
</style>
</head>
<body>
<h1>10 Asynchronous Ajax Requests</h1>
<p>Here come the 10 random numbers..</p>
<div id="theDiv">Wait..</div>
<table id="theTable" summary="the data">
<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td>
<td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>
</table>
<script type="text/javascript">

function fncTrim(str) {
return str.replace(/^\s+|\s+$/g,'');
}
function insertData(vals) {
this.tbl = this.tbl || document.getElementById('theTable');
tbl.rows[0].cells[vals.shift()].firstChild.data = vals.shift();
}

for (var i=0; i < 10; i++) {
var counter = 1, vals = [];
(function (req, items, func) {
var thisone = i;
req.onreadystatechange = function () {
if (req.readyState == 4 && req.status == 200) {
vals.push(thisone, fncTrim(req.responseText) * 1);
if (++counter == Math.floor(items / 2)) {
// start displaying in the table
updating = setInterval(function () {
if (vals.length > 0) {
func(vals);

} else if (counter > items) {
clearInterval(updating);
}
}, 500);
}
}
};
req.open("GET", 'createRandomNumber.php?rnd=' + Math.random(), true);
req.send(null);
}) (new XMLHttpRequest() || new ActiveXObject("Microsoft.XMLHTTP"), 10, insertData);
}

</script>
</body>
</html>

createRandomNumber.php

<?php
sleep(1); // 1 second
echo rand(1, 100);
?>
I suspect that this is more instructional, and hopefully interesting :), than directly applicable. [This follows from a recent thread which triggered my interest.]

AndrewGSW
12-18-2012, 09:42 AM
I am very happy with the algorithm :thumbsup: and the principles involved but I might look to improve it a little:

The worker-function should be passed the 2 shifted values, not the array;
I think this (in this.tbl) is the window object - I'll have to check this - so I'm creating an unnecessary global;
The limit of 10 is defined twice - it should only be defined once, as a variable. However, as it stands, there can be a different number of requests and needed returns..
I should write the code to handle a failed request. It should clear the timer-interval, but should it also cancel any future requests? Maybe I can supply a true/false argument to determine whether future requests should be cancelled.

I'll report back, but welcome any input :thumbsup:

AndrewGSW
12-20-2012, 11:20 PM
The following version handles errors in the requests, and it forces 404 failures on every other request to demonstrate.

<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<style type="text/css">
#theTable { outline: 1px solid gray; }
#theTable td {
width: 50px; height: 50px;
}
</style>
</head>
<body>
<h1>10 Asynchronous Ajax Requests</h1>
<p>Here come the 10 random numbers..</p>
<div id="theDiv">Wait..</div>
<table id="theTable" summary="the data">
<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td>
<td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>
</table>
<script type="text/javascript">

function fncTrim(str) {
return str.replace(/^\s+|\s+$/g,'');
}
function insertData(index, value) {
insertData.tbl = insertData.tbl || document.getElementById('theTable');
insertData.tbl.rows[0].cells[index].firstChild.data = value;
}
var cycle = 10; // total number of requests
for (var i=0; i < cycle; i++) {
var counter = 1, vals = [];
(function (req, items, func) {
var thisone = i;
req.onreadystatechange = function () {
if (req.readyState == 4) {
if (req.status == 200) {
vals.push(thisone, fncTrim(req.responseText) * 1);
if (++counter == Math.floor(items / 2)) {
// start displaying in the table
updating = setInterval(function () {
if (vals.length > 0) {
func(vals.shift(), vals.shift());

} else if (counter > items) {
clearInterval(updating);
}
}, 500);
}
} else {
func(thisone, "Err!" + req.status);
}
}
};
if (thisone % 2) {
req.open("GET", 'createRandomNumber.php?rnd=' + Math.random(), true);
} else {
req.open("GET", 'DoesNotExist.php?rnd=' + Math.random(), true);
}

req.send(null);
}) (new XMLHttpRequest() || new ActiveXObject("Microsoft.XMLHTTP"), cycle, insertData);
}

</script>
</body>
</html>

AndrewGSW
12-21-2012, 01:56 PM
I've got this sussed now :cool:, based on a MultiAjax object and this code fragment:

var thisRun = new MultiAjax({
Page: 'createRandomNumber.php?rnd=',
StartAfter: 5,
Limit: 10,
Delays: 500,
Success: insertData
// Failure: insertData
});

thisRun.Go();
Limit is the total number of ajax requests, StartAfter the responses to await before starting to update the DOM (defaults to half of Limit), Delays the number of millisecs to pause between DOM updates.

Success is the callback to execute to update the DOM, receiving an index number (the requests' index) and the value returned. Failure is an alternative callback to handle failed requests (defaulting to the Success function).

A random number is appended to the request-page so that the same value is not returned each time.


<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<style type="text/css">
#theTable { outline: 1px solid gray; }
#theTable td {
width: 50px; height: 50px;
}
</style>
</head>
<body>
<h1>10 Asynchronous Ajax Requests</h1>
<p>Here come the 10 random numbers..</p>
<div id="theDiv">Wait..</div>
<table id="theTable">
<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td>
<td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>
</table>
<script type="text/javascript">

String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g,'');
};

var insertData = function (index, value) {
insertData.tbl = insertData.tbl || document.getElementById('theTable');
insertData.tbl.rows[0].cells[index].firstChild.data = value;
}

var MultiAjax = function MultiAjax(obj) {
this.Page = obj.Page;
this.Limit = obj.Limit || 10;
this.StartAfter = obj.StartAfter || Math.floor(this.Limit % 2);
this.Success = obj.Success;
this.Failure = obj.Failure || this.Success;
this.Delays = obj.Delays || 500;
this.Results = [];

}
// instance methods
MultiAjax.prototype = {
Go: function () {
var i = 0, that = this;
while (i < this.Limit) {
(function (req) {
var thisOne = i++;
req.onreadystatechange = function () {
if (req.readyState == 4) {
if (req.status == 200) {
that.Results.push(thisOne, (req.responseText).trim() * 1);
if (thisOne == that.StartAfter) {
// start displaying in the table
updating = setInterval(function () {
if (that.Results.length > 0) {
that.Success(that.Results.shift(), that.Results.shift());

} else if (thisOne > that.Limit) {
clearInterval(updating);
}
}, that.Delays);
}
} else {
that.Failure(thisOne, "Err!" + req.status);
}
}
};
if (thisOne % 4) {
req.open("GET", that.Page + Math.random(), true);
} else {
req.open("GET", 'DoesNotExist.php?rnd=' + Math.random(), true);
}
req.send(null);
}) (new XMLHttpRequest() || new ActiveXObject("Microsoft.XMLHTTP"));
}
}
};

var thisRun = new MultiAjax({
Page: 'createRandomNumber.php?rnd=',
StartAfter: 5,
Limit: 10,
Delays: 500,
Success: insertData
// Failure: insertData
});

thisRun.Go();

</script>
</body>
</html>

The only addition would be to allow Page to be an array of pages, but this is straight-forward to incorporate. Job done! Andy.

AndrewGSW
12-21-2012, 02:17 PM
Well, I might as well include the version that can be supplied an array of pages like this:

var thisRun = new MultiAjax({
//Page: 'createRandomNumber.php?rnd=',
Pages: ['createRandomNumber.php?rnd=1235', 'createRandomNumber.php?rnd=0012',
'createRandomNumber.php?rnd=5859', 'createRandomNumber.php?rnd=0024',
'createRandomNumber.php?rnd=2003', 'createRandomNumber.php?rnd=0087',
'createRandomNumber.php?rnd=2587', 'createRandomNumber.php?rnd=0159',
'createRandomNumber.php?rnd=1598', 'createRandomNumber.php?rnd=2335'],
StartAfter: 5,
Limit: 10,
Delays: 500,
Success: insertData
// Failure: insertData
});

thisRun.Go();

<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<style type="text/css">
#theTable { outline: 1px solid gray; }
#theTable td {
width: 50px; height: 50px;
}
</style>
</head>
<body>
<h1>10 Asynchronous Ajax Requests</h1>
<p>Here come the 10 random numbers..</p>
<div id="theDiv">Wait..</div>
<table id="theTable">
<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td>
<td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>
</table>
<script type="text/javascript">

String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g,'');
};

var insertData = function (index, value) {
insertData.tbl = insertData.tbl || document.getElementById('theTable');
insertData.tbl.rows[0].cells[index].firstChild.data = value;
}

var MultiAjax = function MultiAjax(obj) {
this.Page = obj.Page || false;
this.Pages = obj.Pages || false;
this.Limit = obj.Limit || 10;
this.StartAfter = obj.StartAfter || Math.floor(this.Limit % 2);
this.Success = obj.Success;
this.Failure = obj.Failure || this.Success;
this.Delays = obj.Delays || 500;
this.Results = [];

}
// instance methods
MultiAjax.prototype = {
Go: function () {
var i = 0, that = this;
while (i < this.Limit) {
(function (req) {
var thisOne = i++;
req.onreadystatechange = function () {
if (req.readyState == 4) {
if (req.status == 200) {
that.Results.push(thisOne, (req.responseText).trim() * 1);
if (thisOne == that.StartAfter) {
// start displaying in the table
updating = setInterval(function () {
if (that.Results.length > 0) {
that.Success(that.Results.shift(), that.Results.shift());

} else if (thisOne > that.Limit) {
clearInterval(updating);
}
}, that.Delays);
}
} else {
that.Failure(thisOne, "Err!" + req.status);
}
}
};
//if (thisOne % 4) {
// req.open("GET", that.Page + Math.random(), true);
//} else {
// req.open("GET", 'DoesNotExist.php?rnd=' + Math.random(), true);
//}
if (that.Page) {
req.open("GET", that.Page + Math.random(), true);
} else if (that.Pages.length > 0) {
req.open("GET", that.Pages[thisOne], true);
}

req.send(null);
}) (new XMLHttpRequest() || new ActiveXObject("Microsoft.XMLHTTP"));
}
}
};

var thisRun = new MultiAjax({
//Page: 'createRandomNumber.php?rnd=',
Pages: ['createRandomNumber.php?rnd=1235', 'createRandomNumber.php?rnd=0012',
'createRandomNumber.php?rnd=5859', 'createRandomNumber.php?rnd=0024',
'createRandomNumber.php?rnd=2003', 'createRandomNumber.php?rnd=0087',
'createRandomNumber.php?rnd=2587', 'createRandomNumber.php?rnd=0159',
'createRandomNumber.php?rnd=1598', 'createRandomNumber.php?rnd=2335'],
StartAfter: 5,
Limit: 10,
Delays: 500,
Success: insertData
// Failure: insertData
});

thisRun.Go();

</script>
</body>
</html>

AndrewGSW
12-21-2012, 02:59 PM
Whoops! I had to correct it to cancel the setInterval once all the requests have been processed. Last change, I promise :thumbsup:


<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<style type="text/css">
#theTable { outline: 1px solid gray; }
#theTable td {
width: 50px; height: 50px;
}
</style>
</head>
<body>
<h1>10 Asynchronous Ajax Requests</h1>
<p>Here come the 10 random numbers..</p>
<div id="theDiv">Wait..</div>
<table id="theTable">
<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td>
<td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>
</table>
<script type="text/javascript">

String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g,'');
};

var insertData = function (index, value) {
insertData.tbl = insertData.tbl || document.getElementById('theTable');
insertData.tbl.rows[0].cells[index].firstChild.data = value;
}

var MultiAjax = function MultiAjax(obj) {
this.Page = obj.Page || false;
this.Pages = obj.Pages || false;
this.Limit = obj.Limit || 10;
this.StartAfter = obj.StartAfter || Math.floor(this.Limit % 2);
this.Success = obj.Success;
this.Failure = obj.Failure || this.Success;
this.Delays = obj.Delays || 500;
this.Results = [];

}
// instance methods
MultiAjax.prototype = {
Go: function () {
var i = 0, that = this;
while (i < this.Limit) {
(function (req) {
var thisOne = i++;
req.onreadystatechange = function () {
if (req.readyState == 4) {
if (req.status == 200) {
that.Results.push(thisOne, (req.responseText).trim() * 1);
if (thisOne == that.StartAfter) {
// start displaying in the table
updating = setInterval(function () {
this.currentOne = this.currentOne || 0;
this.currentOne++;
if (that.Results.length > 0) {
that.Success(that.Results.shift(), that.Results.shift());

} else if (this.currentOne >= that.Limit) {
clearInterval(updating);
}
}, that.Delays);
}
} else {
that.Failure(thisOne, "Err!" + req.status);
}
}
};
//if (thisOne % 4) {
// req.open("GET", that.Page + Math.random(), true);
//} else {
// req.open("GET", 'DoesNotExist.php?rnd=' + Math.random(), true);
//}
if (that.Page) {
req.open("GET", that.Page + Math.random(), true);
} else if (that.Pages.length > 0) {
req.open("GET", that.Pages[thisOne], true);
}

req.send(null);
}) (new XMLHttpRequest() || new ActiveXObject("Microsoft.XMLHTTP"));
}
}
};

var thisRun = new MultiAjax({
//Page: 'createRandomNumber.php?rnd=',
Pages: ['createRandomNumber.php?rnd=1235', 'createRandomNumber.php?rnd=0012',
'createRandomNumber.php?rnd=5859', 'createRandomNumber.php?rnd=0024',
'createRandomNumber.php?rnd=2003', 'createRandomNumber.php?rnd=0087',
'createRandomNumber.php?rnd=2587', 'createRandomNumber.php?rnd=0159',
'createRandomNumber.php?rnd=1598', 'createRandomNumber.php?rnd=2335'],
StartAfter: 5,
Limit: 10,
Delays: 500,
Success: insertData
// Failure: insertData
});

thisRun.Go();

</script>
</body>
</html>


BTW The random numbers retrieved duplicate occasionally because the PHP random function uses the time of the request to generate the number.

rnd me
12-21-2012, 05:34 PM
here are a few suggestions. ok, several, sorry; i got carried away...



<!DOCTYPE html>
<html> <head>
<title>Page Title</title>
<style type="text/css">
#theTable {
outline: 1px solid gray;
}
#theTable td {
width: 50px;
height: 50px;
}

</style>
</head>
<body>
<h1>10 Asynchronous Ajax Requests</h1>
<p>Here come the 10 random numbers..</p>

<div id="theDiv"><progress>Wait..</progress></div>

<table id="theTable">
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
</table>

<script>

//add "".trim (if needed)
String.prototype.trim = String.prototype.trim || function() {
return this.replace(/^\s+|\s+$/g, '');
};

function insertData (index, value, obj ) {
insertData.tbl = insertData.tbl || document.getElementById(obj.OutputID);
(insertData.tbl.rows[0].cells || "").innerHTML = String(value);
}// end insertData ()



function MultiAjax(obj) {
this.Pages = obj.Pages || [ obj.Page ];
this.StartAfter = obj.StartAfter || Math.floor(this.Pages.length % 2);
this.Success = obj.Success;
this.Base= obj.Base || "";
this.OutputID=obj.OutputID||"theTable";
this.Failure = obj.Failure || this.Success;
this.Delays = obj.Delays || 500;
this.Results = [];
var that=this;
setTimeout(function(){ that.Go(); }, 100);
}//end MultiAjax()


function ajax(url, that){
var req=new XMLHttpRequest();
var thisOne=that.thisOne;

req.onreadystatechange = onReady;
req.open("GET", url, true);
req.send();

function onReady() {

if (req.readyState == 4 && req.status == 200) {
that.Results.push(thisOne, (req.responseText).trim() * 1);
if (thisOne == that.StartAfter) {
// start displaying in the table
that.updating = setInterval(function() {
that.currentOne = that.currentOne || 0;
that.currentOne++;
if (that.Results.length > 0) {
that.Success(that.Results.shift(), that.Results.shift(), that );
console.log("writing", that.results|'');
} else if (that.currentOne >= that.Limit) {
clearInterval(that.updating);
}/* end if(that.Results.length > 0) */
}, that.Delays);
} /* end if(thisOne == that.StartAfter) */
} else if(req.readyState == 4) {
that.Failure( that.thisOne , "Err!" + req.status, that );
} /* end if(req.readyState == 4 .. && 200) */

} /* end onReady() */
}//end ajax()

MultiAjax.prototype = {
Go: function() {
var i = 0, that = this, mx=that.Pages.length;
for (;i < mx; i++) {
that.thisOne=i;
ajax( that.Base + that.Pages[i] , that );
}
}
};

var thisRun = new MultiAjax({
Base: 'createRandomNumber.php?rnd=',
Pages: '1235,0012,5869,0024,2003,0087,2587,0159,1598,2335'.split(',') ,
StartAfter: 5,
Delays: 500,
OutputID: 'theTable',
Success: insertData ,
Failure: insertData
});
</script>
</body>
</html>

changes


@type not needed in html5, removed
added <progress> to look nice, fallback to text in older IEs
re-indent using tabs (saves >1kb of bandwidth)
don't clobber native "".trim() if it's already available.
kill Limit, JS can count
nesting is confusing, so i move the ajax stuff to a re-usable standalone.
- if you want to be fancy, it's called "dependency injection", but it basically means passing [I]that so the function works out-of-loop
change thisOne to that.thisOne so it's bundled, but shadow it in the ajax so each has it's own copy.
mixing this and that is confusing, stick with that for code consistency
while is scary and for indeterminate amounts, use a for loop to iterate collections
move dom ID to the config object for easier re-use or having more than one instance per page.
change brittle firstChild.data to whole-cell .innerHTML to allow rich formatting in future responses.
bind the timeout handle to that for easy persistence
reduce code by coercing a single page into an array, and always using the array routine
why have to call go() each time, let's call it for us.
that's a lot of url, a base and suffix so we can pass "1235,0012,5869,0024,2003,0087,2587,0159,1598,2335".split(",")
- the change shortens the hand-coded part, and is optional if you use full paths and no .Base...
new XMLHttpRequest() || new ActiveXObject("Microsoft.XMLHTTP") will fail (throw) in IE6, the only browser that might need ActiveX. screw IE6.
remove redundant var statements on functions to allow hoisting


2do
-Capitals imply Constructors, properties and methods should be .camelCase'd() instead

AndrewGSW
12-21-2012, 06:29 PM
@rnd me Thank you very much :thumbsup:; I shall study!

Capitals imply Constructors, properties and methods should be .camelCase'd() instead
I was in two minds about this. I would always use the capital to distinguish classes (objects in JS), but I followed an example from Vic ;) about the members' capitalization.

move dom ID to the config object for easier re-use or having more than one instance per page.
I had decided to do this :)

why have to call go() each time, let's call it for us.
I was thinking I could modify the properties of the object and call go() on other occasions(?). But, I suppose it's not the sort of thing that would be called more than once.. Added: I see that it is still possible to call go() separately :)

I like the idea of separating out the Base, and coercing a single page to an array is a good idea.

Cheers again. Andy.

AndrewGSW
12-21-2012, 06:32 PM
Mmm, if there is only a single page will it request it multiple times? I'll have to check :thumbsup:
Edited: Ignore this. I see that you are appending the random numbers to the page(s), which will be required anyway for a single page.

AndrewGSW
12-21-2012, 08:02 PM
The timer-interval is no longer being cleared, because that.Limit is no longer defined(?).

If I can get this working then I could also disable the progress element in some way(?).

Added: ignore me again please:

MultiAjax.prototype = {
Go: function () {
var i = 0, that = this, mx = that.Pages.length;
that.Limit = mx;
for (;i < mx; i++) {

AndrewGSW
12-21-2012, 08:22 PM
..and to signify the progress is complete:

} else if (that.currentOne >= that.Limit) {
clearInterval(that.updating);
var prog = document.getElementsByTagName('progress')[0];
prog.max = prog.value = "100";
A shame that it is not clever enough to stop running back and forth though :rolleyes:

Andy.



EZ Archive Ads Plugin for vBulletin Copyright 2006 Computer Help Forum