I have been learning nodejs with the help of learnyounode and I am stuck on Juggling Async. My solution works with the exception of the hardest part of the problem - it prints the results out of order because some requests complete before others. I am trying to do it without the use of any helper libraries so I hit Google. I found a great tutorial about control flow - http://book.mixu.net/node/ch7.html but found that his solution for Series - an asynchronous for loop only worked properly because his async function waits exactly 1000ms before returning. In my example the time is variable so you won't get the results in the same order. The example then goes into building a control flow library and I feel like while that would work it is out of scope for the learnyounode assignment.
Some things I have tried are:
Making an object that contains the URL, the order and the response data. The problem I had with this is that when I am sitting in the callback for http.get I have no idea for what URL I am getting data. This is actually the fault to most of the things I have tried, I have no idea how to link the data I am getting back to a particular URL in the callback.
Specifically setting a place foe the returned data in the storage array using asyncCounter as the index. So when the event "end" happens I would set it as you see in the code below. Obviously this does not work because calls are finishing at different times.
Eventually I caved and looked at the solution. Here is what I had and I marked what was missing from my solution with asterisks.
var http = require("http");
var storage = [];
var urlList = [];
var asyncCounter = 0;
//Store all the URL's from the command line in an array
for(var i = 2; i < process.argv.length; i++){
urlList.push(process.argv[i]);
}
//This function prints out all the data in the storage array
//The storage array contains the data received from http.get
function AsyncComplete(){
for(var j = 0; j < storage.length; j++){
console.log(storage[j]);
};
};
//Entry function
function main(){
//Do an http get on each of the provided URL's
for(var i = 0; i < urlList.length; i++){
**(function(i){**
http.get(urlList[i], function(response){
var body = "";
//Got a chunk of data!
response.on("data", function(chunk){
body += chunk;
});
//All done with our request.
response.on("end", function (){
//Store the total response from the URL
storage[**i**] = body;
asyncCounter++;
//Check if we can print the results yet
//I want to wait for all the calls to complete so I am using a counter
if(asyncCounter == urlList.length){
AsyncComplete();
}
});
});
**})(i);**
};
};
main();
So I hit up Google again because I have no idea what they are doing and I discovered something called closures. From my understanding, what they did keeps i in scope of the callback - something I could not figure out how to do so thought was impossible and abandoned early on.
My question is how exactly this works - particularly why they have (i) where they do and what purpose it serves. The solution does not work without that (i). Why is it that you can just throw (function(i){ somewhere? This is something I never would have thought of doing in a million years.
I found https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures and that seems like it could contain the answer so I am going to read through that in the meantime and see if I can come up with something.
Thanks in advance for your time!
Steve.
This is called an Immediate Invoked Function Expression (IIFE) and is simply an anonymous function which is invoked immediately. The parameter i which is passed to this IIFE is put on the call stack and is a different one than the outer i of the for loop. You can imagine it like creating a snapshot of the current i.
In JavaScript variables which are declared with "var" are function scoped. So, all callbacks would access the same i (which is not what you want). By passing it to the IIFE (which is a function and thus introduces an own scope for the variable i) a new variable is created for each callback. It gets more obvious if you give the parameter of the IIFE another name. For example:
(function(savedI) {
// ...
})(i)
By naming the parameter i, the original i is shadowed. This means that you cannot access the "outer" i inside of the IIFE.
A common alternative to using an IIFE, would be to use map instead. For example:
_.range(urlList.length).map(function(i) {
// ...
})
_.range is a utility function from underscore and gives an array reaching from 0 until urlList.length. The passed function will receive a parameter i which won't be overriden (like the for loop did it in your version).
Yet another possibility to avoid the issue is declaring the variable via let which is possible in EcmaScript 6 or higher. This introduces block scoped variables. You can read more about it here. For example:
for(let i = 0; i < urlList.length; i++){
// ..
}