2

I have to run some code after some predefined irregular intervals after users takes some action like a button click.

The intervals could be [1, 1.2, 2.05, 2.8, ...., 10.56], [0.2, 1.5, 3.9, 4.67, 6.8, .., 12.0] or something else. So, the function should be called after 1 second, 1.2 seconds, 2.05 seconds and so on after the initial button click.

How can I do that in JavaScript?

Real Noob
  • 1,369
  • 2
  • 15
  • 29

2 Answers2

2

You use setTimeout inside a function that gets called by that setTimeout:

// intervals in seconds
const intervals = [1, 1.2, 2.05, 2.8, 10.56];

(function runIrregularly(runThisCode) {
  if (intervals.length > 0) {
    // get (and remove) the first element from the array:
    const timeout = intervals.shift();

    // and schedule the next call to run in the future
    setTimeout(
      () => runIrregularly(runThisCode),
      timeout * 1000
    );
  }

  // then run our real function
  runThisCode();
})(doSomething);

function doSomething() {
  console.log(Date.now());
}

Now, while we're using shift() to get elements from the start of the array, you should at that point go "hm, so it doesn't even care that it's an array?" and indeed: because this code doesn't iterate over anything, you can even replace that with a generating function that comes up with values as needed:

function getNextInterval() {
  // do whatever you like here
  return some number;
}

(function runIrregularly(runThisCode) {
  if (intervals.length > 0) {
    const timeout = getNextInterval();

    ...

(You could even get fancy by using a generator function if you wanted)

Also, we don't have to call the function "as we declare it" of course, we can also declare and use it separately:

function runIrregularly(runThisCode) {
  if (intervals.length > 0) {
    ...
  }
  runThisCode();
}

// and then at some later point in the code

runIrregularly(doSomething);

And finally, if you need this to cycle, rather than run once, we can combine shift with push, and copy the interval list when we start:

function runIrregularly(runThisCode, intervals) {
  // get and remove the first element from the array
  const timeout = intervals.shift();

  // then push it back on as the last element.
  intervals.push(timeout);

  setTimeout(
    () => runIrregularly(runThisCode, intervals),
    timeout * 1000
  );
  
  runThisCode();
}

// and then later:
runIrregularly(doSomething, intervals.slice());

With the interval.slice() being necessary so that we leave the original array pristine (otherwise using for other functions will make those not start at your first interval, but "wherever we are in the cycle" based on however many calls are running).

How to capture and stop the timeouts, I leave as an exercise to the reader (there are plenty of questions/posts about that on the web/SO to find).

Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
  • Thanks Mike. :) I have just one question. Is this time currently cumulative? For example, first call at 1 second and the next at 1 + 1.2 = 2.2 seconds etc? – Real Noob Jan 21 '22 at 17:12
  • You should be able to answer that yourself already by looking at how this code does its thing. (even just running it would give you an answer, but looking at when things happen in this function will tell you _why_) – Mike 'Pomax' Kamermans Jan 21 '22 at 17:13
  • It does seem to be cumulative to me just wanted to confirm. From what I can understand, the next call is scheduled 1.2 seconds after the call at 1 second. :) – Real Noob Jan 21 '22 at 17:16
  • don't look at the numbers: look at the code flow. We schedule a _single_ timeout for however long that takes. Only once that has timed out, we schedule the _next_ timeout. – Mike 'Pomax' Kamermans Jan 21 '22 at 17:17
  • 1
    Thanks Mike. :) Your comments inside the code snippets helped me understand the whole process properly. – Real Noob Jan 21 '22 at 17:33
  • I have to say, really clean solution with `.shift()`! – Marco Jan 21 '22 at 18:05
2

You can also achieve this with async/await in combination with setTimeout:

setTimeout(function() {

}, 1000);

Would execute function after 1 second has passed.

You can achieve your goal by nesting setTimeout calls (see answer by Mike) or with promises (async/await):

function delayExecution(n) {
    return new Promise((resolve) => {
        setTimeout(resolve, n)
    })
}

async function runIrregularly(fn, delays) {
    for (const delay of delays) {
        // suspend execution by `delay` amounts of seconds
        await delayExecution(delay * 1000)

        fn()
    }
}

runIrregularly(() => {
    console.log(Date.now(), "Hello")
}, [1, 2, 3])
Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
Marco
  • 7,007
  • 2
  • 19
  • 49