0

i am using a library called timer.js because I want to loop through an array, but want to have a delay between the iterations

this is a link to the library's github: https://github.com/husa/timer.js

what I did, is created a file form the min version of timer.js and linked to it in the body of html before my main js file like so:

<script src="./timer.js" charset="utf-8"></script>
<script src="./script.js" charset="utf-8"></script>

then inside the main js file I created an array of numbers and I just want to loop through them while having an interval between the console.logs:

var timer = new Timer();

var nums = [1,2,3,4]

for (var i = 0; i < nums.length; i++){
  timer.start(5).on('end', function(){
    console.log(i);
  })
}

when I open the console log, I only get a '4' logged. Why is the for loop not working for other indexes. Also, is there any way to recreate this with a forEach function?

Thank you

krabbos
  • 47
  • 4
  • thats calling `timer.start(5)` at the same time 4 times, so after `5` units of time, it should console.log 4 times, and by that time, `i` will be 4. – George Apr 02 '18 at 18:59
  • what does that library do that you can't do with setTimeout or setInterval ? – George Apr 02 '18 at 19:00
  • It is a very common mistake in JavaScript. You cannot instantiate functions inside a for loop like this, it does not work as you expect – Patrick Hund Apr 02 '18 at 19:03
  • 1
    Possible duplicate of [JavaScript closure inside loops – simple practical example](https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example) – Patrick Hund Apr 02 '18 at 19:03

2 Answers2

1

I want to loop through an array, but want to have a delay between the iterations

Set the duration based on the iteration:

duration*iteration

setTimeout can achieve the delay, duration is in milliseconds, let's say 2 seconds (2000ms), and lets add 1 to the iteration so it has a delay for the first iteration:

const nums = [1,2,3,4]

nums.forEach((n, i)=>{
  setTimeout(()=>(
    console.log(i)
  ), 2000*(i+1))
});
George
  • 2,330
  • 3
  • 15
  • 36
  • This will produce similar results, but it's crucial to understand that the forEach will not wait between iterations. It's simply scheduling all of the logs at once, each with a multiplied delay. – sripberger Apr 02 '18 at 19:37
1

You're running into a few common pitfalls when dealing with asynchronous code.

The first thing to realize is that your for loop is not waiting for each iteration to complete. It's simply invoking timer.start(5) four times, as quickly as it can. In order to do this, you can't use a for loop. Your best bet is to use something from the very popular async library instead.

The second thing to realize is that you're invoking start(5) on the same instance repeatedly, causing it to reset to five seconds each time. As a result, only one of your scheduled callbacks is being invoked, five seconds after the loop has completed. This is really less a misunderstanding of async code, and more a misunderstanding of the timer.js library itself. You can fix this by just creating new timers in the loops, but for your case timer.js is a bit overkill. You can do exactly what you want with native setTimeout calls.

The third thing to realize is that the callback you're sending to on() does not store the current value of i. It simply stores a reference to that variable, so any changes to that variable will ultimately be reflected in the log.

During the last iteration of the loop, the value of i is actually 3, not 4. This is because 4 is not < nums.length. So, the only callback to execute should log a 3, right?

Not so fast, though. The last thing the loop always does after each iteration is increment the value of i. So, i goes from 3 to 4, then checks if it's < nums.length, and since it's not, it exits. Then, five seconds later, your callback logs the value of i, which is now 4.

This is a property of something called a closure in Javascript. I definitely recommend reading that link, because the concept is pretty crucial for using the language effectively.

Anyways, since you're probably looking for correct code here's how I would do what you're attempting, using async and setTimeout:

var nums = [1,2,3,4];

async.eachSeries(nums, (num, done) => {
    setTimeout(() => {
        console.log(num);
        done();
    }, 5000);
});
sripberger
  • 1,682
  • 1
  • 10
  • 21
  • hi, can you please write a Promise version which I can try in my browser? – George Apr 02 '18 at 19:48
  • You can try it as-is in a browser. You just need to include this script first: https://raw.githubusercontent.com/caolan/async/master/dist/async.min.js I'll go ahead and do one with Promises, though. I'll use a different library than `async` though, because async is mainly designed for callback-based code. – sripberger Apr 02 '18 at 19:52
  • Unfortunately I can't seem to find a browser-ready promise-oriented async library. There's this but you'd need Browserify to use it: https://www.npmjs.com/package/pasync – sripberger Apr 02 '18 at 20:17
  • It'd be pretty easy to wrap this small subset of async in an promise-based api, but I'm going to avoid adding that to my answer because I feel it will only add confusion. – sripberger Apr 02 '18 at 20:19