setTimeout not working inside forEach

2020-05-18 12:35发布

问题:

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);
});

回答1:

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



回答2:

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.