Async calls with async response in NodeJS
I'm building my first project with NodeJS in these days but I'm a bit confused on a task I believe it's a simple one, I guess the problem is my lack of knowledge of these async approach but I cannot find the answer anywhere.
I've a simple loop iterating over an array and for any element, based on some rule, I'll call a function or another. Now some operation will be faster than others so I could end up with a function on element N returning sooner than the function on element N-1. To make it s开发者_运维问答imple something like this
for (var i = 0 ; i < 10 ; i++) {
if (i%2 === 0) {
setTimeout(function(i) {
console.log(i);
}, 2000);
}
else { console.log(i); }
}
so any even number will be printed with 2 seconds lag while the odd numbers will be printed immediately. Anyway running it I get
1
3
5
7
9
<<2 seconds break>>
undefined
undefined
undefined
undefined
undefined
looks like the even value is "lost". How can I pass the value making sure the function won't lose the input value? Am I missing something?
Thanks, Mauro
Your setTimeout
argument function is declared to take a single parameter, i
. When setTimeout
calls the function with no arguments, as it does, the parameter is thus set to undefined
.
This would seem to be be slightly better, as it no longer shadows the outer i
variable you were originally trying to reference...
setTimeout(function() {
console.log(i);
}, 2000)
...but if you run it, you'll find it prints 10
5 times, because every function you create is referencing the same i
variable, whose value will be 10
when the loop exit condition becomes true
and it terminates.
Creating a closure which holds the value of i
as it was during the loop in which each setTimout
argument function was created will do the trick:
setTimeout((function(i) {
return function() {
console.log(i);
}
})(i), 2000)
Renaming the argument to the Immediately-Invoked Function Expression we just used might help make things clearer:
setTimeout((function(loopIndex) {
return function() {
console.log(loopIndex);
}
})(i), 2000)
We're:
- Creating a function which takes a single argument and returns another function.
- Immediately calling the "outer" function, passing it the current value of
i
(which is not an object, so is effectively passed by value). - The "outer" function returns the "inner" function, which is passed as the argument to
setTimeout
.
This works because:
Creating a function in JavaScript creates a new scope to hold the function's argument variables and any other variables declared within it using the
var
keyword. Think of this like an invisible object with properties corresponding to the variable names.All functions hold a reference to the scope in which they were defined, as part of their scope chain. They still have access to this scope even if it's no longer "active" (e.g. when the function it was created for returns). The "inner" function was created in a scope which contained a
loopIndex
variable, set to the value ofi
at the time the IIFE was called. So when you try to reference a variable namedloopIndex
from inside the inner function, it first checks its own scope (and doesn't find aloopIndex
there) then starts walking up its scope chain - first, it checks the scope in which it was defined, which does contain aloopIndex
variable, the value of which is passed toconsole.log()
.
That's all a closure is - a function which has access to the scope in which it was defined, even if the function the scope was created for has finished executing.
Although you usually use bind
to ensure that the value of this
inside a callback function is what you want it to be, you can also use it to modify the arguments passed to the bound function. In your case, the code could look something like this (I changed some variable names so it was clearer what's going on):
for (var i = 0 ; i < 10 ; i++) {
if (i % 2 === 0) {
setTimeout(function(num) {
console.log(num);
}.bind(this, i), 2000);
} else {
console.log(i);
}
}
In this case, the function has .bind(this, i)
attached to it. The this
is just for padding; the first argument to bind
is always what you want this
to resolve to when inside the bound function. After that, however, we can pass in any values that we want to be added to the arguments of the bound function--in this case, we want the current value of i
(when the function is bound) to be passed to the function.
You can read more about bind
at the MDN Docs.
setTimeout(function(i) {
console.log(i);
}, 2000);
Functions called from setTimeout will not be called with any parameter. You need to create a closure.
Check out this question and the accepted answer.
(This is not a node.js-specific problem)
精彩评论