18

I'm confused with using setTimeout and the each iterator. How can I rewrite the following so that the console outputs each name after a delay of 5 seconds? Currently the code below prints all the names at once after 5 seconds. I would like to:

1) wait 5 seconds, then print kevin
2) wait 5 seconds, then print mike
3) wait 5 seconds, then print sally

var ary = ['kevin', 'mike', 'sally'];

_(ary).each(function(person){

  setTimeout(function(){
    console.log(person);
  }, 5000);    

});
Kevin
  • 3,441
  • 6
  • 34
  • 40

8 Answers8

19

You have three basic options:

  1. For Loop + setTimeout
    ... initialize everyone immediately, but stagger the start times based on the index position so they don't all go at the same time.
  2. setTimeout + conditional recursion
    ... check back in every n seconds - and I'll tell you if you need to do another
  3. setInterval + conditional clearInterval
    ... keep running every n seconds - until I tell you to stop

Here's each one of those fleshed out with a working example:

1. For Loop + setTimeout

A couple notes on this one. This is kind of like starting a relay race and handing out instructions ahead of time to each runner to start at precisely 5:00 and 5:02 and 5:04, regardless of whether the person behind them finished a long time ago or hasn't yet arrived.

Also, you can't use a regular for i=0 loop, because the for operator does not define new function scope. Thus object values set within each for loop will apply across iterations. By the time setTimeout is called, it will only use the most recent value. So we need a closure to store the value within each loop. I've used Array.prototype.forEach(), but if you want to use the forEach implementations in jQuery or Underscore, that'll work too.

function ArrayPlusDelay(array, delegate, delay) {
 
  // initialize all calls right away
  array.forEach(function (el, i) {
    setTimeout(function() {
        // each loop, call passed in function
        delegate( array[i]);

      // stagger the timeout for each loop by the index
      }, i * delay);
  })
 
}

// call like this
ArrayPlusDelay(['a','b','c'], function(obj) {console.log(obj)},1000)

2. setTimeout + conditional recursion

For the bottom two options, we're making our own loops, so we'll have to keep track of the index ourselves, initializing at zero and incrementing throughout.

For this one, we'll a) call setTimeout which will run once, b) evaluate the array at the index position, c) check if there are more elements in the array and if so, start over at (a).

   
function ArrayPlusDelay(array, delegate, delay) {
  var i = 0
  
  function loop() {
     // each loop, call passed in function
      delegate(array[i]);
      
      // increment, and if we're still here, call again
      if (i++ < array.length - 1)
          setTimeout(loop, delay); //recursive
  }

  // seed first call
  setTimeout(loop, delay);
}

// call like this
ArrayPlusDelay(['d','e','f'], function(obj) {console.log(obj)},1000)

3. setInterval + conditional clearInterval

NOTE: The function setInterval will run forever once called. It's return value when initially set will provide a reference to the interval, so it is often combined with the function clearInterval to optionally shut it off down the road

function ArrayPlusDelay(array, delegate, delay) {
  var i = 0
  
   // seed first call and store interval (to clear later)
  var interval = setInterval(function() {
     // each loop, call passed in function
      delegate(array[i]);
      
        // increment, and if we're past array, clear interval
      if (i++ >= array.length - 1)
          clearInterval(interval);
  }, delay)
  
}

ArrayPlusDelay(['x','y','z'], function(obj) {console.log(obj)},1000)

3* Secret Fourth Option (Best One)

Options 1 & 2 are risky because, once you set off that train, there's no way to call it off down the road (save closing the browser). If you have a large array or a heavy load in your delegate, it might be nice to provide some recourse if you need it. By saving the reference from setInterval, we'll have constant access to the iterating function. We just need to return the interval object above and save it when calling our array plus delay function.

function ArrayPlusDelay(array, delegate, delay) {
  var i = 0
  
   // seed first call and store interval (to clear later)
  var interval = setInterval(function() {
     // each loop, call passed in function
      delegate(array[i]);
      
        // increment, and if we're past array, clear interval
      if (i++ >= array.length - 1)
          clearInterval(interval);
  }, delay)
  
  return interval
}

var inter = ArrayPlusDelay(['x','y','z'], function(obj) {console.log(obj)},1000)

Then if we ever want to clear it later on, just throw this in the console:

clearInterval(inter);

All 3 Demos in jsFiddle

Similar Stack Overflow Questions:

KyleMit
  • 30,350
  • 66
  • 462
  • 664
17

You could create a variable called offset that makes the timer wait 5 seconds more for each person in the array, like so:

var ary = ['kevin', 'mike', 'sally'];

var offset = 0;
_(ary).each(function(person){

  setTimeout(function(){
    console.log(person);
  }, 5000 + offset);    
 offset += 5000;
});
LonelyWebCrawler
  • 2,866
  • 4
  • 37
  • 57
  • Can you describe how this works? I'm guessing this works because the each iterator fired off 3 "events" and each event has a timeout of 5, 10, and 15 seconds respectively? And my code fired off 3 "events" with each event lasting just 5 seconds? – Kevin Jun 22 '13 at 00:57
  • 1
    Exactly. `each` runs three times, and each time `offset` gets 5000 larger, so that the timeout for each consecutive person is 5 seconds later. – LonelyWebCrawler Jun 22 '13 at 00:59
7

You could do

var ary = ['kevin', 'mike', 'sally'];

_(ary).each(function(person, index){

  setTimeout(function(){
    console.log(person);
  }, index * 5000);    
});

Without increasing the timeout value, you would initialize all setTimeouts with the exact same value (thats why you see what you see).

jAndy
  • 231,737
  • 57
  • 305
  • 359
1

each is usually better for things that happen immediately.

Instead, if you don't mind changing the array, you can use it as a queue:

var ary = ['kevin', 'mike', 'sally'];

setTimeout(function loop() {
    console.log(ary.shift());

    if (ary.length)
        setTimeout(loop, 5000);
}, 5000);

It keeps on calling loop 5 seconds in the future until there's nothing left in the queue.

Casey Chu
  • 25,069
  • 10
  • 40
  • 59
0

array.forEach accepts a callback function which means it already creates a new function scope and as the nature of setTimeout is non-blocking which return immediately you just need to increment delay for callback function execution with every iteration:

var ary = ['kevin', 'mike', 'sally'];

ary.forEach(function(person, index){
    setTimeout(function(){
        console.log(person);
    }, 5000 * (index + 1));   
})

In case if you want to achieve the same with for loop you can make use of IIFE or let keyword

IIFE example:

var ary = ['kevin', 'mike', 'sally'];

for(var i = 1; i <= ary.length; i++){
    (function(i){
        setTimeout(function(){
            console.log(ary[i - 1]);
        }, 5000 * i); 
    })(i)
}

let keyword example: [ECMAScript 6]

var ary = ['kevin', 'mike', 'sally'];

for(let i = 1; i <= ary.length; i++){
    setTimeout(function(){
        console.log(ary[i - 1]);
      }, 5000 * i); 
}
0

The easiest way:

const ary = ['kevin', 'mike', 'sally'];

ary.forEach((item, index) => {
    setTimeout(() => {
        console.log(item)
    }, index * 5000)
})

"kevin" has index 1, so it will be called after 5 seconds (1 * 5000 ms)

"mike" has index 2, so it will be called after 10 seconds (2 * 5000 ms)

"sally" has index 3, so it will be called after 15 seconds (3 * 5000 ms)

xDrd
  • 3
  • 1
0

let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
array.forEach((el, i) => {setTimeout(function() {console.log(array[i]);}, i * 1000);});
Moritz Ringler
  • 9,772
  • 9
  • 21
  • 34
  • The difference between this question and the others seems to be that this one is not printing user names, which was requested in the question. – Moritz Ringler Feb 20 '23 at 21:11
-1

You could just use setInterval() with a simple increase-by-one counter.

var ary = ['kevin', 'mike', 'sally'];

var i=0;
setInterval(function(){
    console.log(ary[i]);
    i++;
}, 5000);

But note that this will start throwing errors after i becomes greater than two. You need to have some kind of validation there and make sure you clear the interval.

php_nub_qq
  • 15,199
  • 21
  • 74
  • 144