Async loops and intervals

Draft
This page is not complete.

Here we look at the traditional methods JavaScript has available for running code asychronously after a set time period has elapsed, or at a regular interval (e.g. once per second), talk about what they are useful for, and look at their inherent issues.

Prerequisites: Basic computer literacy, a reasonable understanding of JavaScript fundamentals.
Objective: To understand asynchronous loops and intervals and what they are useful for.

Introduction

For a long time, JavaScript has made available a number of functions that allow you to asynchronously execute code after a certain time interval has elapsed, and repeat code in a loop endlessly until you tell it to stop. These are:

  • setTimeout() — Execute a specified block of code once after a specified time has elapsed.
  • setInterval() — Execute a specified block of code repeatedly with a fixed time delay between each call.
  • requestAnimationFrame() — the modern version of setInterval(); executes a specified block of code before the browser next repaints the display, allowing an animation to be run at a suitable framerate regardless for the environment it is being run in.

Being async, these functions again run on a separate event loop, so do not block other code in your document. These are ideal for running constant animations and other background processing on a web site or application. In the following sections we will show you how they can be used.

setTimeout()

As we said before, setTimeout() executes a specified block of code once after a specified time has elapsed. It takes two mandatory parameters, and further optional ones:

  • A function to run, or a reference to function defined elsewhere.
  • A number representing the time interval in milliseconds to wait before executing the code.
  • One or more values that represent any parameters you want to pass to the function when it is run.

In the following example, the browser will wait two seconds before executing the anonymous function and presenting the alert message (see it running live, and see the source code):

let myGreeting = setTimeout(function() {
 alert('Hello, Mr. Universe!');
}, 2000)

Note that we're not required to write anonymous functions. We can give our function a name, and can even define it somewhere else and pass a function reference to the  setTimeout(). The following two versions of our code snippet are equivalent to the first one:

// With a named function
let myGreeting = setTimeout(function sayHi() {
 alert('Hello, Mr. Universe!');
}, 2000)

// With a function defined separately
function sayHi() {
 alert('Hello Mr. Universe!');
}

let myGreeting = setTimeout(sayHi, 2000);

Passing parameters to a setTimeout() function

Any parameters that we want to pass to the function being run inside the setTimeout() have to be passed to it as additional parameters at the end of the list. For example, we could refactor our previous function so that it will say hi to whatever person's name is passed to it:

function sayHi(who) {
 alert('Hello ' + who + '!');
}

The name of the person to say hello to can then be passed into the setTimeout() call as a third parameter:

let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe');

Clearing timeouts

Finally, if a timeout has been called, you can cancel it before it the specified time has elapsed by calling clearTimeout(), passing it the identifier of the setTimeout() call as a parameter. So to cancel our above timeout, you'd do this:

clearTimeout(myGreeting);

Note: See greeter-app.html for a slightly more involved demo that allows you to set the name of the person to say hello to in a form, and cancel the greeting using a separate button (see the source code also).

setInterval()

setTimeout() works perfectly when we need to run the code once after a set period of time. But what happens when we need to run the code over and over again, for example in the case of an animation?

This is where setInterval() comes in. This works in a very similar way to setTimeout(), except that the function you pass to it as the first parameter is executed repeatedly at an interval equal to the number of milliseconds provided as the second parameter, rather than once. You can also pass any parameters required by the function being executed in as subsequent parameters of the setInterval() call.

So let's look at an example. The following function creates a new Date() object, extracts a time string out of it using toLocaleTimeString(), and then displays it in the UI. We then run it once per second using setInterval(), creating the effect of a digital clock that updates once per second (see this live, and also see the source):

function displayTime() {
   let date = new Date();
   let time = date.toLocaleTimeString();
   document.getElementById('demo').textContent = time;
}

const createClock = setInterval(countTime, 1000);

Clearing intervals

setInterval() keeps running a task forever, unless we do something about it — we may well want a way to stop such tasks, otherwise we may end up getting errors when the browser can't complete any further versions of the task. We can do this in the same sort of way as we stopped timeouts — by passing the identifier of the setInterval() call to the clearInterval() function:

const myInterval = setInterval(myFunction, 2000);

clearInterval(myInterval);

Active learning: Creating your own stopwatch!

With this all said, we've got a challenge for you. Take a copy of our setInterval-clock.html example, and modify it so that you create your own simple stop watch.

You need to display a time as before, but in this example, you need:

  • A "Start" button to start the stop watch running.
  • A "Stop" button to pause/stop it.
  • A "Reset" button to reset the time back to 0.
  • The time display to show the number of seconds elapsed, rather than the actual time.

Here's a few hints for you:

  • You can structure and style the button markup however you like; just make sure you use semantic HTML, with hooks to allow you to grab the button references using JavaScript.
  • It is easier to create this example without using a Date() object.
  • You probably want to create a variable that starts at 0, then increments by one every second using a constant loop.
  • You also want to calculate the number of hours, minutes, and seconds as separate values, and then show them together in a string after each loop iteration. From the second counter, you can work out each of these.
  • How would you calculate them? Have a think about it:
    • The number of seconds in an hour is 3600.
    • The number of minutes will be the amount of seconds left over when all of the hours have been removed, divided by 60.
    • The number of seconds will be the amount of seconds left over when all of the minutes have been removed.
  • You'll want to include a leading zero on your display values if the amount is less than 10, to look more like a traditional clock/watch.
  • To pause the stopwatch, you'll want to clear the interval. To reset it, you'll want to set the counter back to 0 and then immediately update the display.

Note: If you get stuck, you can find our version here (see the source code also).

Things to keep in mind about setTimeout() and setInterval()

There are a few things to keep in mind when working with setTimeout() and setInterval(). Let's review these now.

Recursive Timeouts

There is another way we can use setTimeout(): We can call it recursively to run the same code repeatedly, instead of using setInterval().

The below example uses a recursive setTimeout() to run the passed function every 100 milliseconds:

let i = 1;

setTimeout(function run() {
  console.log(i);
  i++;
  setTimeout(run, 100);
}, 100);

Compare the above example to the following one — this uses setInterval() to accomplish the same effect:

let i = 1;

setInterval(function run() {
  console.log(i);
  i++
}, 100);

How do recursive setTimeout() and setInterval() differ?

The difference between the two versions of the above code is a subtle one.

  • Recursive setTimeout() guarantees a 100ms delay between the executions; the code will run and then wait 100 milliseconds before it runs again. The interval will be the same regardless of how long the code takes to run.
  • The example using setInterval() does things somewhat differently. The interval we choose includes the time taken to execute the code we want to run in. Let's say that the code takes 40 milliseconds to run — the interval then ends up being only 60 milliseconds.

When your code has the potential to take longer to run than the time interval you’ve assigned, it’s better to use recursive setTimeout() — this will keep the time interval constant between executions regardless of how long the code takes to execute, and you won't get errors.

Immediate timeouts

Using 0 as the value for setTimeout() schedules the execution of the passed function as soon as possible but only after the main code thread has been run.

For instance, the code below (see it live) outputs an alert containing "Hello", then an alert containing "World" as soon as you click OK on the first alert.

setTimeout(function() {
  alert('World');
}, 0);

alert('Hello');

This can be useful in cases where you want to set a block of code to run as soon as all of the main thread has finished running; put it on the async event loop, so it will run straight afterwards.

Clearing with clearTimeout() or clearInterval()

clearTimeout() and clearInterval() use the same list of entries to clear from. Interestingly enough, this means that you can use either method to clear a setTimeout() or setInterval().

For consistency, you should use clearTimeout() to clear setTimeout() entries and clearInterval() to clear setInterval() entries. This will help to avoid confusion.

requestAnimationFrame()

requestAnimationFrame() is a specialized looping function created for running animations in the browser. It is basically the modern version of setInterval() — it executes a specified block of code before the browser next repaints the display, allowing an animation to be run at a suitable framerate regardless of the environment it is being run in.

It was created in response to perceived problems with setInterval(), for example it doesn't run at a framerate optimized for the device, sometimes drops frames, continues to run even if the tab is not the active tab or the animation is scrolled off the page, etc. Read more about this on CreativeJS.

Note: You can find examples of using requestAnimationFrame() elsewhere in the course — see for example Drawing graphics, and Object building practice.

The method takes as an argument a callback to be invoked before the repaint. This is the general pattern you'll see it used in:

function draw() {
   // Drawing code goes here
   requestAnimationFrame(draw);
}

draw();

The idea is that you define a function in which your animation is updated (e.g. your sprites are moved, score is updated, data is refreshed, or whatever), then you call it to start the process off. At the end of the function block you call requestAnimationFrame() with the function reference passed as the parameter, and this instructs the browser to call the function again on the next display repaint. This is then run continuously, as we are calling requestAnimationFrame() recursively.

Note: If you want to perform some kind of simple constant DOM animation, CSS Animations are probably faster as they are calculated directly by the browser's internal code rather than JavaScript. If however you are doing something more complex and involving objects that are not directly accessible inside the DOM (such as 2D Canvas API or WebGL objects), requestAnimationFrame() is the better option in most cases.

How fast does your animation run?

The smoothness of your animation is directly dependent on your animation's frame rate and it is measured in frames per second (fps). The higher this number is, the smoother your animation will look, to a point.

Since most screens have a refresh rate of 60Hz, the fastest frame rate you can aim for is 60fps when working with web browsers. However, more frames means more processing, which can often cause stuttering and skipping. This is what is meant by dropping frames, or jank.

If you have a monitor that gives you 60Hz and you want to achieve 60fps you have about 16.7 milliseconds to execute your animation code. This is a reminder that we need to be mindful of the amount of code that we try to run for each animation loop.

requestAnimationFrame() always tries to get as close to this magic 60fps value as possible, although sometimes it isn't possible — if you have a really complex animation and you are running it on a slow computer, your frame rate will be less. requestAnimationFrame() will always do the best it can with what it has available.

How does requestAnimationFrame() differ from setInterval() and setTimeout()?

Let's talk a little bit more about how the requestAnimationFrame() method differs from the other methods we looked at earlier. Looking at our code from above:

function draw() {
   // Drawing code goes here
   requestAnimationFrame(draw);
}

draw();

Let's now see how we'd do the same thing using setInterval():

function draw() {
   // Drawing code goes here
   requestAnimationFrame(draw);
}

setInterval(draw, 17);

As we said before, we don't specify a time interval for requestAnimationFrame(); it just runs it as fast and smoothly as possible in the current conditions; the browser doesn't waste time running it if the animation is offscreen for some reason, etc.

setInterval() on the other hand requires an interval to be specified. We arrived at our final value of 17 via the formula 1000 milliseconds / 60Hz, and then rounded it up. Rounding up is a good idea, as if you rounded down the browser might try to run the animation faster than 60fps, and it wouldn't make any difference to the smoothness of the animation anyway. As we said before, 60Hz is the standard refresh rate.

requestAnimationFrame() is supported in slightly more recent browsers than setInterval()/setTimeout() — most interestingly it is available in Internet Explorer 10 and above. So unless you need to support older versions of IE with your code, there is little reason to not use requestAnimationFrame().

A simple example

Enough with the theory; let's go through and build our own requestAnimationFrame() example. We're going to create a simple "spinner animation", the kind you might see displayed in an app when it is busy connecting to the server, etc.

  1. First of all, grab a basic HTML template such as this one.

  2. Put the following <div> element inside the <body>. This contains a spinner character that will act as our spinner for this simple example.

  3. Apply the following CSS to the HTML template in whatever way you prefer. This sets a red background on the page, sets the <body> height to 100% of the <html> height, and centers the <div> inside the <body>, horizontally and vertically.

    html {
      background-color: white;
      height: 100%;
    }
    
    body {
      height: inherit;
      background-color: red;
      margin: 0;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    
    div {
      display: inline-block;
      font-size: 10rem;
    }
  4. Insert a <script> element just above the </body> tag.

  5. Insert the following JavaScript inside your <script> element. Here we're storing a reference to the <div> inside a constant, setting a rotateCount variable to 0, and setting an uninitialized variable that will later be used to contain a reference to the requestAnimationFrame() call.

    const spinner = document.querySelector('div');
    let rotateCount = 0;
    let rAF;
  6. Below the previous code, insert a draw() function that will be used to contain our animation code:

    function draw() {
    
    }
  7. Inside draw(), add the following lines. Here we rotate the spinner character by the value of degrees set inside the rotateCount variable, then increment the amount of rotation by 2 on each frame of the animation:

    spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
    rotateCount += 2;
  8. Below the previous line inside draw(), add the following block — this checks to see if the value of rotateCount is above 359 (e.g. 360, a full circle). If so, it sets it back to 0 and the circle animation can start again. Note that this isn't strictly necessary, but it is easier to work with values of 0-359 degrees than values like "12456 degrees".

    if(rotateCount > 359) {
      rotateCount = 0;
    }
  9. At the very bottom inside the draw() function, insert the following line. This is the key to the whole operation — we are setting the variable we defined earlier to an active requestAnimation() call that takes the draw() function as its parameter. This starts the animation off, constantly running the draw() function at a rate of as close to 60fps as possible.

    rAF = requestAnimationFrame(draw);

You can test the framerate out — try timing how long it takes for the spinner to do one rotation, and you'll see that it should take about 6 seconds (360/60).

Note: You can find this example live on GitHub (see the source code also).

Clearing a requestAnimationFrame() call

Clearing a requestAnimationFrame() call can be done by calling the corresponding cancelAnimationFrame() method (note, "cancel" not "clear" as with the "set..." methods), passing it the identifier of the requestAnimationFrame() call to cancel:

cancelAnimationFrame(rAF);

Active learning: Starting and stopping our spinner

In this exercise, we'd like you to test out the cancelAnimationFrame() method by taking our previous example and updating it, adding an event listener to start and stop the spinner when the mouse is clicked anywhere on the page.

Some hints:

  • A click event handler can be added to most elements, including the document <body>.
  • You'll want to add a tracking variable to check whether the spinner is spinning or not, clearing the animation frame if it is, and calling it again if it isn't.

Try this yourself first; if you get really stuck, check out of our live example and source code.

Throttling a requestAnimationFrame() animation

One limitation of requestAnimationFrame() is that you can't choose your framerate. This isn't a problem most of the time, as generally you want your animation to run as smoothly as possible, but what about when you want to create an old school, 8-bit-style animation?

This was a problem for example in the Monkey Island-inspired walking animation from our Drawing Graphics article:

In this example we have to animate both the position of the character on the screen, and the sprite being shown. There are only 6 frames in the sprite's animation; if we showed a different sprite frame for every frame displayed on the screen by requestAnimationFrame(), Guybrush would move his limbs too fast and the animation would look ridiculous. We therefore throttled the rate at which the sprite cycles it's frames using the following code:

if(posX % 13 === 0) {
  if(sprite === 5) {
    sprite = 0;
  } else {
    sprite++;
  }
}

So we are only cycling a sprite once every 13 animation frames. OK, so it's actually about every 6.5 frames, as we update posX (character's position on the screen) by two each frame:

if(posX > width/2) {
  newStartPos = -((width/2) + 102);
  posX = Math.ceil(newStartPos / 13) * 13;
  console.log(posX);
} else {
  posX += 2;
}

This is the code that works out how to update the position in each animation frame.

The method you use to throttle your animation will depend on your particular code. For example, in our spinner example we could make it appear to move slower by only increasing our rotateCount by one on each frame instead of two.

Active learning: a reaction game

For our final section of this article, we'll create a 2-player reaction game. Here we have two players, each of whom play the game with one of the control keys — A and L.

When the Start button is pressed, a spinner like the one we saw earlier is displayed for a random amount of time between 5 and 10 seconds. After that time, a message will appear saying "PLAYERS GO!!" — once this happens, the first player to press their control button will win the game.

Let's work through this.

  1. First of all, download the starter file for the app — this contains the finished HTML structure and CSS styling, giving us a gameboard that shows the two players' information (as seen above), but with the spinner and results paragraph displayed on top of one another. We just have to write the JavaScript code.

  2. Inside the empty <script> element on your page, start by adding the following lines of code that define some constants and variables we'll need in the rest of the code:

    const spinner = document.querySelector('.spinner p');
    const spinnerContainer = document.querySelector('.spinner');
    let rotateCount = 0;
    let rAF;
    const btn = document.querySelector('button');
    const result = document.querySelector('.result');

    In order, these are:

    1. A reference to our spinner, so we can animate it.
    2. A reference to the <div> element that contains the spinner, used for showing and hiding it.
    3. A rotate count — how much we want to show the spinner rotated on each frame of the animation.
    4. An uninitialized variable to later store the requestAnimationFrame() call that animates the spinner.
    5. A reference to the Start button
    6. A reference to the results paragraph
  3. Next, below the previous lines of code, add the following function. This simply takes two numerical inputs and returns a random number between the two. We'll need this to generate a random timeout interval later on.

    function random(min,max) {
      var num = Math.floor(Math.random()*(max-min)) + min;
      return num;
    }
  4. Next add in the draw() function, which animates the spinner. This is exactly the same as the version seen in the simple spinner example we looked at earlier:

     function draw() {
       spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
       rotateCount += 2;
       if(rotateCount > 359) {
         rotateCount = 0;
      }
      rAF = requestAnimationFrame(draw);
    }
  5. Now it is time to set up the initial state of the app when the page first loads. Add the following two lines, which simply hide the results paragraph using display: none;, and hide the spinner container by setting z-index to -1, thereby moving it behind the main UI (if we just set it to display: none;, it would still be there sat in front of the UI, rendering the button unclickable by the mouse).

    result.style.display = 'none';
    spinnerContainer.style.zIndex = -1;
  6. We'll also define a reset() function, which sets the app back to the original state required to start the game again after it has been played. Add the following at the bottom of your code:

    function reset() {
      btn.style.display = 'block';
      result.textContent = '';
      result.style.display = 'none';
    }
  7. OK, enough preparation, let's make the game playable! Add the following block to your code. The start() function calls draw() to start the spinner spinning, display it on top of the UI, hides the Start button so we can't mess up the game by starting it multiple times concurrently, and runs a setTimeout() call that runs a setEndgame() function after a random interval between 5 and 10 seconds has passed. We also add an event listener to our button to run the start() function when it is clicked.

    btn.addEventListener('click', start);
    
    function start() {
      draw();
      spinnerContainer.style.zIndex = 1;
      btn.style.display = 'none';
      setTimeout(setEndgame, random(5000,10000));
    }

    Note: You'll see that in this example we are calling setTimeout() without giving it an identifying name (so not let myTimeout = setTimeout(functionName, interval)). This works and is fine, as long as you don't need to clear your interval/timeout at any point. If you do, you'll need to give it an identifier.

    The net result of the previous code is that when the Start button is pressed, the spinner is shown and the players are made to wait a random amount of time before they are then asked to press their button. This last part is handled by the setEndgame() function, which we should define next.

  8. So add the following function to your code next:

    function setEndgame() {
      cancelAnimationFrame(rAF);
      spinnerContainer.style.zIndex = -1;
      result.style.display = 'block';
      result.textContent = 'PLAYERS GO!!';
    
      document.addEventListener('keydown', keyHandler);
    
      function keyHandler(e) {
        console.log(e.keyCode);
        if(e.keyCode === 65) {
          document.removeEventListener('keydown', keyHandler);
          result.textContent = 'Player 1 won!!';
          setTimeout(reset, 5000);
        } else if(e.keyCode === 76) {
          document.removeEventListener('keydown', keyHandler);
          result.textContent = 'Player 2 won!!';
          setTimeout(reset, 5000);
        }
      };
    }

    Stepping through this:

    1. First we cancel the spinner animation with cancelAnimationFrame() (it is always good to clean up unneeded processes), and hide the spinner container.
    2. Next we display the results paragraph and set its text content to "PLAYERS GO!!" to signal to the players that they can now press their button to win.
    3. We then attach a keydown event listener to our document — when any button is pressed down, the keyHandler() function is run.
    4. Inside keyHandler(), we include the event object as a parameter (represented by e) — its keyCode property contains the numerical key code of the key that was just pressed, and we can use this to respond to specific key presses with specific actions.
    5. We first log e.keyCode to the console, which is a useful way of finding out the keyCode value of different keys you are pressing.
    6. When e.keyCode is 65 ("A"), we display a message to say that Player 1 won, and when e.keyCode is 76, we display a message to say Player 2 won.
    7. Regardless of which one of the player control keys was pressed, we remove the keydown event listener using removeEventListener() so that once the winning press has happened, no more keyboard input is possible to mess up the final game result. We also use setTimeout() to call reset() after 5 seconds — as we explained earlier, this function resets the game back to its original state so that a new game can be started.

That's it, you're all done.

Note: If you get stuck, check out our version of the reaction game (see the source code also).

Conclusion

So that's it — all the essentials of async loops and intervals covered in one neat little article. You'll find these methods useful in a lot of situations, but take care not to overuse them — heavy and intensive animations can really slow down a page if you're not careful.

In this module

  • Introducing Async JavaScript
  • Async loops and intervals
  • Handling async operations gracefully with Promises
  • Easier async code with async and await

Document Tags and Contributors

Contributors to this page: chrisdavidmills
Last updated by: chrisdavidmills,