Reputation: 2677
Just can't seem to figure this out, how to prevent the outer loop continuing until the inner loop has executed using setTimeout:
function $(s) { return document.getElementById(s); }
var i = 0;
for (i=0; i<6; i++){
$('t'+i).className = 'show';
cl('t'+i);
for (var j=0; j<50; j++){
cl('b'+i+'='+j+'%');
setTimeout(function(){ $('b'+i).style.width = j+'%';},200);
}
}
This little bit of code is supposed to first make elements t0 visible, and then set the width of another element b0 in 1% steps with a time interval of 200ms, and then continue with t1,b1, t2,b2 etc etc.
But there's no 200ms delay, the whole code executes immediately.
--- EDIT ---
I have not explained very well, here's what I want to do:
1. show element Ax
2. increase element Bx width by 1% every 200ms until 50%
3. wait until element Bx reaches 50% before continuing
4. increment x
5. goto 1
Upvotes: 0
Views: 212
Reputation: 484
setTimeout(function () { alert('Refreshing…'); }, 2000);
It means after two seconds (2000 ms), show alert
Upvotes: 0
Reputation: 1075249
Two problems:
i
and j
valuesi
and j
valuesThe main problem is that the function you're passing into setTimeout
has an enduring reference to the i
and j
variables, not copies of them as of when the function was created. That means all of the functions will see the value of i
and j
as of when they run, which will be 6
and 50
respectively. This is called "closing over" those variables (and the function is called a "closure").
The usual way to fix this is to create the functions in a way that they close over something that doesn't change. There are a couple of ways to do that; my favorite is to use a factory function:
function $(s) { return document.getElementById(s); }
var i = 0;
for (i=0; i<6; i++){
$('t'+i).className = 'show';
cl('t'+i);
for (var j=0; j<50; j++){
cl('b'+i+'='+j+'%');
setTimeout(makeHandler(i, j), 200);
}
}
function makeHandler(ivalue, jvalue) {
return function(){ $('b'+ivalue).style.width = jvalue+'%';};
}
Now we call makeHandler
, and it returnns us a function that closes over ivalue
and jvalue
, which won't change. Or a refinement of the above which lets us dispose of the maker function when we're done with it:
function $(s) { return document.getElementById(s); }
var i = 0;
var makeHandler = function(ivalue, jvalue) {
return function(){ $('b'+ivalue).style.width = jvalue+'%';};
};
for (i=0; i<6; i++){
$('t'+i).className = 'show';
cl('t'+i);
for (var j=0; j<50; j++){
cl('b'+i+'='+j+'%');
setTimeout(makeHandler(i, j), 200);
}
}
makeHandler = undefined;
If you can rely on ES5 features (because of the environment you're targeting, or because you've included an ES5 shim), you can get much the same effect using the new Function#bind
. Function#bind
creates a new function just like makeHandler
does, but there's always the possibility the engine can optimize a bit:
function $(s) { return document.getElementById(s); }
var i = 0;
for (i=0; i<6; i++){
$('t'+i).className = 'show';
cl('t'+i);
for (var j=0; j<50; j++){
cl('b'+i+'='+j+'%');
setTimeout(handler.bind(undefined, i, j), 200);
}
}
function handler(){
$('b'+ivalue).style.width = jvalue+'%';
}
The first argument to bind
is what this
should be in the function; in our case we don't care, so I've specified undefined
(which means the function will get this
referencing the global object — window
, on browsers — unless this is strict mode code, in which case this
will actually be undefined
).
All of your functions are being scheduled to run 200ms after the above code. And so they will. :-) If you want to space them out, you'll want to increase that 200ms for each call to setTimeout
. We can just multiply by i
and j
:
setTimeout(makeHandler(i, j), (200 * i * j) + 200);
Now the first one will run after 200ms, the second one 200ms later, etc. The whole thing will take about a minute to complete. This assumes you want the first element to grow, then the next, then the next, as opposed to all six of them growing in parallel with each other.
Alternately, you might want to have each function call its successor. That's probably what I'd do. So rather than scheduling 300 function calls, just schedule one and when it happens, schedule the next:
function $(s) { return document.getElementById(s); }
// Our counters are here
var i = 0, j = 0;
// This handles the outer portion of the loop (mostly)
function outer() {
$('t'+i).className = 'show';
cl('t'+i);
j = 0;
// Schedule the first inner portion 200ms from now
setTimeout(inner, 200);
}
// This handles the inner portion of the loop (mostly)
function inner() {
// Do this bit
$('b'+i).style.width = j+'%';
++j;
if (j < 50) {
// Continue the inner loop in 200ms
setTimeout(inner, 200);
}
else {
// Set up next outer loop
j = 0;
++i;
if (i < 6) {
// Not done yet, keep going
setTimeout(outer, 200);
}
}
}
// Kick it off
setTimeout(outer, 200);
In the above, I've also moved these lines:
$('t'+i).className = 'show';
cl('t'+i);
...into the delayed code, which I suspected was appropriate.
Upvotes: 4
Reputation: 173652
It's because you're not closing over the i
and j
:
setTimeout(function(i, j) {
return function() {
$('b'+i).style.width = j+'%';
}
}(i, j), 200);
Upvotes: 3