31

Is there an easy way to slow down the iteration in a forEach (with plain javascript)? For example:

var items = document.querySelector('.item');

items.forEach(function(el) {
  // do stuff with el and pause before the next el;
});
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
Kirk Ross
  • 6,413
  • 13
  • 61
  • 104

8 Answers8

96

What you want to achieve is totally possible with Array#forEach — although in a different way you might think of it. You can not do a thing like this:

var array = ['some', 'array', 'containing', 'words'];
array.forEach(function (el) {
  console.log(el);
  wait(1000); // wait 1000 milliseconds
});
console.log('Loop finished.');

... and get the output:

some
array          // one second later
containing     // two seconds later
words          // three seconds later
Loop finished. // four seconds later

There is no synchronous wait or sleep function in JavaScript that blocks all code after it.

The only way to delay something in JavaScript is in a non–blocking way. That means using setTimeout or one of its relatives. We can use the second parameter of the function that we pass to Array#forEach: it contains the index of the current element:

var array = ['some', 'array', 'containing', 'words'];
var interval = 1000; // how much time should the delay between two iterations be (in milliseconds)?
array.forEach(function (el, index) {
  setTimeout(function () {
    console.log(el);
  }, index * interval);
});
console.log('Loop finished.');

Using the index, we can compute when the function should be executed. But now we have a different problem: the console.log('Loop finished.') is executed before the first iteration of the loop. That's because setTimout is non–blocking.

JavaScript sets the timeouts in the loop, but it doesn't wait for the timeouts to complete. It just continues executing the code after the forEach.

To handle that, we can use Promises. Let's build a promise chain:

var array = ['some', 'array', 'containing', 'words'];
var interval = 1000; // how much time should the delay between two iterations be (in milliseconds)?
var promise = Promise.resolve();
array.forEach(function (el) {
  promise = promise.then(function () {
    console.log(el);
    return new Promise(function (resolve) {
      setTimeout(resolve, interval);
    });
  });
});

promise.then(function () {
  console.log('Loop finished.');
});

There is an excellent article about Promises in conjunction with forEach/map/filter here.


I gets trickier if the array can change dynamically. In that case, I don't think Array#forEach should be used. Try this out instead:

var array = ['some', 'array', 'containing', 'words'];
var interval = 2000; // how much time should the delay between two iterations be (in milliseconds)?

var loop = function () {
  return new Promise(function (outerResolve) {
    var promise = Promise.resolve();
    var i = 0;
    var next = function () {
      var el = array[i];
      // your code here
      console.log(el);
      if (++i < array.length) {
        promise = promise.then(function () {
          return new Promise(function (resolve) {
            setTimeout(function () {
              resolve();
              next();
            }, interval);
          });
        });
      } else {
        setTimeout(outerResolve, interval);
        // or just call outerResolve() if you don't want to wait after the last element
      }
    };
    next();
  });
};

loop().then(function () {
  console.log('Loop finished.');
});

var input = document.querySelector('input');
document.querySelector('button').addEventListener('click', function () {
  // add the new item to the array
  array.push(input.value);
  input.value = '';
});
<input type="text">
<button>Add to array</button>
PeterMader
  • 6,987
  • 1
  • 21
  • 31
  • 1
    This works great. Now, say the server was adding elements to the array while the promise loop was running. Is there an easy way - inside the loop - to query and append the array? Say it was var array = document.querySelectorAll('.all-at-the-moment-but-stay-tuned‌​'); – Kirk Ross Aug 06 '17 at 04:01
  • Possible, but more complex. – PeterMader Aug 06 '17 at 10:03
  • shouldn't the resolve passed through the function be executed? : setTimeout(resolve(), interval); Instead of : setTimeout(resolve, interval); – John Yepthomi May 13 '21 at 14:58
  • still works, was able to slow down a foreach that calls a client api that was complaining about how fast the calls were coming in. – Robert Cadmire Dec 15 '21 at 20:01
9

You need to make use of setTimeout to create a delay and have a recursive implementation

You example should look like

var items = ['a', 'b', 'c']
var i = 0;
(function loopIt(i) {
  setTimeout(function(){
      // your code handling here
      console.log(items[i]);
      if(i < items.length - 1)  loopIt(i+1)
    }, 2000);
})(i)
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
6

With JS Promises and asnyc/await syntax, you can make a sleep function that really works. However, forEach calls each iteration synchronously, so you get a 1 second delay and then all of the items at once.

const items = ["abc", "def", "ghi", "jkl"];

const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

items.forEach(async (item) => {
  await sleep(1000);
  console.log(item);
});

What we can do instead is use setInterval and clearInterval (or setTimeout but we're using the former) to make a timed forEach loop like so:

function forEachWithDelay(array, callback, delay) {
  let i = 0;
  let interval = setInterval(() => {
    callback(array[i], i, array);
    if (++i === array.length) clearInterval(interval);
  }, delay);
}

const items = ["abc", "def", "ghi", "jkl"];

forEachWithDelay(items, (item, i) => console.log(`#${i}: ${item}`), 1000);
4

I think recursion offers the simplest solution.

function slowIterate(arr) {
  if (arr.length === 0) {
    return;
  }
  console.log(arr[0]); // <-- replace with your custom code 
  setTimeout(() => {
    slowIterate(arr.slice(1));
  }, 1000); // <-- replace with your desired delay (in milliseconds) 
}

slowIterate(Array.from(document.querySelector('.item')));
David L. Walsh
  • 24,097
  • 10
  • 61
  • 46
2

You can use async/await, Promise constructor, setTimeout() and for..of loop to perform tasks in sequence where a duration can be set set before a task is performed

(async() => {

  const items = [{
    prop: "a",
    delay: Math.floor(Math.random() * 1001)
  }, {
    prop: "b",
    delay: 2500
  }, {
    prop: "c",
    delay: 1200
  }];

  const fx = ({prop, delay}) =>
    new Promise(resolve => setTimeout(resolve, delay, prop)) // delay
    .then(data => console.log(data)) // do stuff

  for (let {prop, delay} of items) {
    // do stuff with el and pause before the next el;
    let curr = await fx({prop, delay});
  };
})();
guest271314
  • 1
  • 15
  • 104
  • 177
2

You can make a promise and use it with a for, the example has to be in an async / await function:

    let myPromise = () => new Promise((resolve, reject) => {
      setTimeout(function(){
        resolve('Count')
      }, 1000)
    })
  
    for (let index = 0; index < 100; index++) {
      let count = await myPromise()
      console.log(`${count}: ${index}`)    
    }
Simas Joneliunas
  • 2,890
  • 20
  • 28
  • 35
sarten8
  • 23
  • 1
  • 6
  • This is the correct answer. The rest of the answers are overcomplicated. You can simulate the "pause" in a loop in javascript using the "for" loop like the above code example by Sarten8. – newbreedofgeek Feb 15 '23 at 07:49
0

First of all you have to change your code:

var items = document.querySelectorAll('.item'), i;

for (i = 0; i < items.length; ++i) {
  // items[i] <--- your element
}

You can loop over Arrays easily in JavaScript with forEach, but unfortunately, it's not that simple with the results of a querySelectorAll

Read more about It here

I can advice you to read this answer to find a right solution for sleep

Ali Mamedov
  • 5,116
  • 3
  • 33
  • 47
-1

Generators

function* elGenLoop (els) {
  let count = 0;

  while (count < els.length) {
    yield els[count++];
  }
}

// This will also work with a NodeList
// Such as `const elList = elGenLoop(document.querySelector('.item'));`
const elList = elGenLoop(['one', 'two', 'three']);

console.log(elList.next().value); // one
console.log(elList.next().value); // two
console.log(elList.next().value); // three

This gives you complete control over when you want to access the next iteration in the list.

monners
  • 5,174
  • 2
  • 28
  • 47