50

I have a forEach that calls a function. There needs to be a delay between each time it is called. I've put it inside a setTimeout inside the forEach. It isn't respecting the timeout after the first wait. Instead it is waiting once, then running all at once. I've set the timeout to 5 seconds and I am using a console to confirm. 5 seconds of wait, then several foobar console logs all at once.

Why am I getting this behavior?

var index = 0;
json.objects.forEach(function(obj) {
    setTimeout(function(){
        console.log('foobar');
        self.insertDesignJsonObject(obj, index);
    }, 5000);
});
Goose
  • 4,764
  • 5
  • 45
  • 84
  • 9
    Why do you think each call to `setTimeout` would be sequential? Hint: All the timers run concurrently. – Dark Falcon Jun 22 '16 at 20:25
  • 1
    If you want to have a sequence like `first object ... 5secs ... second object ... 5 secs ... third object` you would have to "loosen" the loop, i.e. remove the forEach, set counter to 0, get the element with index "counter", start a timeout, in the callback of the timeout set counter to 1, get element with index 1 etc etc – devnull69 Jun 22 '16 at 20:30
  • This might be happening because of the asynchronous nature. – Prasheel Jun 22 '16 at 20:33
  • Where is the `index` variable defined? – Barmar Jun 22 '16 at 20:49
  • Possible duplicate of [All timers created in loop (with setTimeout) fire at same time?](http://stackoverflow.com/questions/9982293/all-timers-created-in-loop-with-settimeout-fire-at-same-time) – Sebastian Simon Jun 22 '16 at 20:53
  • @Barmar added, thanks. – Goose Jun 22 '16 at 20:54
  • @Xufox That is essentially the same, but I feel that question is a bit too messy and Greasemonkey specific. I'd prefer a better Conatical question to accept as a dupe. – Goose Jun 22 '16 at 20:56
  • @Goose Is `index` supposed to increment for each object? – Barmar Jun 22 '16 at 20:56
  • @Barmar, yes, that happens inside the function. – Goose Jun 22 '16 at 20:57
  • You mean inside `insertDesignJsonObject`? Why does it take the index as a parameter if it updates the global variable? – Barmar Jun 22 '16 at 20:58
  • @Barmar it was a simplified code snippet for replication. Sorry but these questions are getting off topic from the question asked. – Goose Jun 22 '16 at 21:01
  • 1
    @Goose The problem is that the simplified code is missing the parts needed to explain the actual problem. Sometimes you can simplify too much, so the question becomes hard to answer properly. – Barmar Jun 22 '16 at 21:03
  • @Barmar, that isn't the problem. The answers given fix the issue. Dark Falcon, devnull69, The Dembinski, and Jason Xie all correctly understood. I added the index, there should be no issues understanding the question asked. – Goose Jun 22 '16 at 21:14

4 Answers4

51

What Jason said is totally correct in his answer but I thought I would give it a shot, to better clarify.

This is actually a classic closure problem. Typically it would look something like:

for(var i = 0; i < 10; i++){
    setTimeout(function(){
        console.log(i);
    },i * 1000)
}

The novice would expect the console to show:

0
(0 seconds pass...)
1
(1 second passes...)
2
(etc..)

But that just isn't the case! What you would actually see is the number 10 getting logged 10 times (1x per second)!

"Why does that happen?" Great question. Closure scope. The for loop above lacks closure scope because in javascript, only functions (lambdas) have closure scope!

See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

However! Your attempt would have achieved the desired output if you had tried this:

    json.objects.forEach(function(obj,index,collection) {
        setTimeout(function(){
            console.log('foobar');
            self.insertDesignJsonObject(obj, index);
        }, index * 5000);
    });

Because you have access to the "closur-ed" index variable - you can rely on its state being the expected state when the function (lambda) is invoked!

Other Resources:

How do JavaScript closures work?

http://javascript.info/tutorial/closures

http://code.tutsplus.com/tutorials/closures-front-to-back--net-24869

The Dembinski
  • 1,469
  • 1
  • 15
  • 24
  • 2
    Actually, I would think a closer-reading novice to expect your first piece of code to print `0 (no time passes) 1 (1 second passes) 2 (2 seconds pass)...` since you have `1 * 1000` as the time portion :). – Heretic Monkey Jun 22 '16 at 21:06
  • 1
    Great answer! but there is one mistake here. `What you would actually see is the number 10 getting logged 10 times all-at-once!` is not all-at-once, it'll be one log per second. – Mengo Jun 22 '16 at 21:08
  • @MikeMcCaughan Ah! Good point, made the correction. I +1 your comment :) – The Dembinski Jun 22 '16 at 21:11
  • @JasonXie Also great catch. Fixed. – The Dembinski Jun 22 '16 at 21:13
42

setTimeout is async. What it does is register a callback function and put it in background which will be triggered after delay. Whereas forEach is synchronous function. So what your code did is register callbacks "all at once" that each will be triggered after 5 seconds.

Two ways to avoid it:

Have an index to set the timer.

json.objects.forEach(function(obj, index) {
    setTimeout(function(){
      // do whatever
    }, 5000 * (index + 1));
});

This way the delay factor is based on the index of your objects, so even you register them at same time, it will trigger based on their own delay. index + 1 to keep the same result as in question since it starts at 0.

setInterval to loop your objects

var i = 0;
var interval = setInterval(function(){
    var obj = json.objects[i];
    // do whatever
    i++;
    if(i === json.objects.length) clearInterval(interval);
}, 5000);

setInterval is similar to setTimeout, although it triggers periodically based on interval. In here we access object and update the index inside of the interval function. Also don't forget to clear interval in the end.

The difference between those two is setInterval only registered one function compare to setTimeout registered as many as number of items in the list.

jbyrd
  • 5,287
  • 7
  • 52
  • 86
Mengo
  • 1,177
  • 1
  • 10
  • 25
2

forEach runs synchronously, looping through all of your elements and scheduling a timer callback for each of them five seconds later. So five seconds later, all those callbacks happen.

Here in 2022, there are two primary approaches here:

  1. Use chained setTimeout with an index variable.

  2. Use an async function, await, and a loop.

Here's an example of #1:

const json = {
    objects: [
        {name: "first object"},
        {name: "second object"},
        {name: "third object"},
        {name: "fourth object"},
        {name: "fifth object"},
    ],
};

function insertDesignJsonObject(obj, index) {
    console.log(obj.name, index);
}

let index = 0; // The index of the next element to show
function doNext() {
    // Do this one, increment the index
    /*self.*/insertDesignJsonObject(json.objects[index], index);
    ++index;

    // If we still have one to do, do it after a delay
    if (index < json.objects.length) {
        setTimeout(doNext, 1000); // 5000 for a five-second delay
    }
}
doNext();

Here's #2:

const json = {
    objects: [
        {name: "first object"},
        {name: "second object"},
        {name: "third object"},
        {name: "fourth object"},
        {name: "fifth object"},
    ],
};

function insertDesignJsonObject(obj, index) {
    console.log(obj.name, index);
}

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

(async () => {
    for (let index = 0; index < json.objects.length; ++index) {
        // Wait to do this one until a delay after the last one
        if (index > 0) {
            await delay(1000); // 5000 for five seconds
        }

        // Do this one
        /*self.*/insertDesignJsonObject(json.objects[index], index);
    }
})();
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
1
var i =0;
img_list.forEach(function(item){
    setTimeout(function(){
        img_class.setAttribute('src',item)
        console.log(item)
    },2000 * i++)
})