Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”

This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money

The first thing that came to mind was to use setInterval. It was so easy to implement the countdown requirement and start a pleasant day of moving blocks. However, after learning about requestAnimationFrame, I found that it was much better to use it

The countdown is implemented with setInterval

Here comes the simplest code for implementing a countdown using setInterval

const totalDuration = 10 // Countdown to 10 seconds
let duration = totalDuration
let countDownInterval = null

function countDown() {
    countDownInterval = setInterval(() = > {
        duration = duration - 1
        console.log(duration)
        if (duration <= 0) {
            clearInterval(countDownInterval)
        }
    }, 1000)
}

countDown()

Copy the code

SetInterval implementation DEMO can be clicked

Disadvantages of using the setInterval implementation

But is the countdown implemented using setInterval accurate? We can do a DEMO verification, and it can be seen that there are deviations from -2ms to 7ms per second. “Many articles on the Internet say that setInterval is set to 1000ms each time, but the actual execution is greater than 1000ms.” However, after I use it as DEMO implementation and set the 1000ms interval, In fact, it may be less than 1000ms, so using setInterval to realize the deviation per second is not a simple accumulation directly. We can calculate the difference between the end time and the start time after the countdown ends

It’s possible that the time interval isLess than 1000 msthe

The figure below is the time difference for the countdown of 100s8ms

SetInterval deviation calculation DEMO can be clicked

const totalDuration = 100; // 100s
let duration = totalDuration;
let countDownInterval = null;
let startTime = new Date().getTime();
let endTime = startTime;
let prevEndTime = startTime
let timeDifferance = 0; // Countdown deviation per 1s, in ms
let totalTimeDifferance = 0; // Total countdown deviation, in ms

function countDown() {
  countDownInterval = setInterval(() = > {
    duration = duration - 1;
    prevEndTime = endTime
    endTime = new Date().getTime();
    console.log('Current execution time:${endTime}, first execution time:${startTime}`);
    console.log('Difference between current and first execution:${endTime - startTime}`)
    timeDifferance = endTime - prevEndTime
    document.getElementById("app").innerText = duration;
    document.getElementById("differance").innerText = timeDifferance;
    if (duration <= 0) {
      totalTimeDifferance = endTime - startTime - totalDuration * 1000;
      console.log('Cumulative time difference:${totalTimeDifferance}`)
      clearInterval(countDownInterval); }},1000);
}

countDown();
Copy the code

However, if you add a thread blocking code in front of it, the variance per second can be surprisingly large

 setInterval( () = > {
     let n = 0;
     while(n++ < 1000000000);
 }, 0);
Copy the code

After adding this thread blocking code, the countdown is only 10 seconds, and the cumulative time difference is up to 8s

In a word, we should try to correct the error value. The general idea is that the next time the countdown is executed, the execution deviation value is advanced, so the time interval of each execution is not fixed at 1s, so setTimeout is used instead of setInterval

Use setTimeout to implement the countdown

According to the above, in order to correct the calculation error of the countdown, we can modify the above code to

const totalDuration = 100; // 100s
let duration = totalDuration;
let countDownInterval = null;
let startTime = new Date().getTime();
let endTime = startTime;
let prevEndTime = startTime;
let timeDifferance = 0; // Countdown deviation per 1s, in ms
let totalTimeDifferance = 0; // Total countdown deviation, in ms
let interval = 1000; // 1s

function countDown() {
  duration = duration - 1;
  endTime = new Date().getTime();
  timeDifferance = endTime - prevEndTime;
  console.log('Current countdown:${duration}, deviation value of execution per second:${timeDifferance}`)
  let nextTime = interval - timeDifferance
  // If the next execution takes longer than the current cycle, special processing is required
  if (nextTime < 0) {
     nextTime = 0
  }
  document.getElementById("nextTime").innerText = nextTime;
  if (duration <= 0) {
    totalTimeDifferance = endTime - startTime - totalDuration * 1000;
    console.log('Cumulative execution deviation value:${totalTimeDifferance}`)
    clearTimeout(countDownInterval);
  } else {
    countDownInterval = setTimeout(() = > countDown(), nextTime);
  }
}

countDownInterval = setTimeout(() = > countDown(), interval);
Copy the code

Note: If the next execution interval is less than 0 seconds, you need to do something special

As shown in the figure below, the cumulative time difference can be controlled within 1s

Also add thread blocking code countdown 10s, error value can be reduced to 2.5s, indicating that the optimization effect is very obvious, of course, the actual process is so serious blocking is very extreme, the focus is no longer to reduce the delay, but to focus on the reconstruction of the blocking code

SetTimeout implementation countdown implementation DEMO can be clicked

Is there a better solution than using setTimeout to implement the countdown?

What is requestAnimationFrame

RAF mainly redraws the page according to the refresh frequency of the monitor (60Hz or 75Hz), and synchronously redraws the page according to this refresh frequency, that is, the maximum multiple drawing frequency of 60 or 75 times is about 1s. Calculated according to 60Hz, each redrawing is about 16.67ms. If setInterval is set to a frequency lower than 16.67ms, it will cause transition drawing problems. If setInterval is set to a frequency higher than 16.67ms, it may drop frames

We want to know is that the window. RequestAnimationFrame (the callback) before the next redrawing execution time is in the browser calls the callback function to obtain the latest animation in the RAF calculation results

SetTimeout versus requestAnimationFrame

RequestAnimationFrame will stop executing when the TAB in modern browsers is not activated

The current TAB page is not in the “active” state, setTimeout will continue to perform tasks in the background, we can add log to the setTimeout DEMO above, when we switch to another TAB, the current TAB page is out of focus, setTimeout will continue to execute. However, using requestAnimationFrame (RAF), you can see the log console that the time period we switch out is not executed, which can improve performance

Of course setTimeout can be optimized by listening for the window.visibilityChange() event

SetTimeout, setInterval belongs to JS engine, RAF belongs to GUI engine

When I was working on the project, I found that if I was loading a large graph, the countdown made by setInterval would stall and suddenly speed up. The reason is that the JS thread and GUI thread in JS are mutually exclusive. If GUI thread is executed for a long time, the JS thread will be blocked. We’ll do a DEMO to verify that

Use requestAnimationFrame instead of setInterval to implement the countdown scheme

const totalDuration = 10 * 1000;
let requestRef = null;
let startTime;
let prevEndTime;
let prevTime;
let currentCount = totalDuration;
let endTime;
let timeDifferance = 0; // Countdown deviation per 1s, in ms
let interval = 1000;
let nextTime = interval;

setInterval(() = > {
  let n = 0;
  while (n++ < 1000000000);
}, 0);

const animate = (timestamp) = > {
  if(prevTime ! = =undefined) {
    const deltaTime = timestamp - prevTime;
    if (deltaTime >= nextTime) {
      prevTime = timestamp;
      prevEndTime = endTime;
      endTime = new Date().getTime();
      currentCount = currentCount - 1000;
      console.log("currentCount: ", currentCount / 1000);
      timeDifferance = endTime - startTime - (totalDuration - currentCount);
      console.log(timeDifferance);
      nextTime = interval - timeDifferance;
      // If it is too slow, execute the next loop immediately
      if (nextTime < 0) {
        nextTime = 0;
      }
      console.log('The time to perform the next rendering is:${nextTime}ms`);
      if (currentCount <= 0) {
        currentCount = 0;
        cancelAnimationFrame(requestRef);
        console.log('Cumulative deviation value:${endTime - startTime - totalDuration}ms`);
        return; }}}else {
    startTime = new Date().getTime();
    prevTime = timestamp;
    endTime = new Date().getTime();
  }
  requestRef = requestAnimationFrame(animate);
};

requestRef = requestAnimationFrame(animate);

Copy the code

RequestAnimationFrame implementation DEMO is clickable

Even when executing thread-blocking code, the deviation is only 98ms

Special attention points

Compatibility issues

RequestAnimationFrame is compatible with IE10 and above, so don’t worry too much about compatibility

Destroy the listening event when you leave the page

  • The RAF is destroyed using cancelAnimationFrame

  • Use webkitCancelRequestAnimationFrame superior to use webkitCancelAnimationFrame wekit kernel browsers

Implement the countdown case using React Hooks and requestAnimationFrame

If you want to deal with the deviation value, you can refer to the above written processing oh

import React, { useState, useEffect, useRef } from "react";
const [count, setCount] = useState<number>(0)
const [duration, setTotalDuration] = useState<number>(0)
const requestRef = useRef(null);
const previousTimeRef = useRef(null);
const currentCountRef = useRef<number>(0);

const animate = time= > {
    if(previousTimeRef.current ! = =undefined) {
      const deltaTime = time - previousTimeRef.current;
      if (deltaTime > 1000) {
        if (currentCountRef.current > 0) {
          previousTimeRef.current = time;
          setCount(prevCount= > {
            currentCountRef.current = prevCount - 1000
            return prevCount - 1000
          });
        } else {
          setCount(0)
          cancelAnimationFrame(requestRef.current);
          return}}}else {
      previousTimeRef.current = time;
    }
    requestRef.current = requestAnimationFrame(animate);
  }

  useEffect(() = > {
    const totalDuration = 60 * 1000
    setCount(totalDuration)
    setTotalDuration(totalDuration)
  }, [])

  useEffect(() = > {
    if (duration <= 0) {
      return
    }
    currentCountRef.current = duration
    previousTimeRef.current = undefined
    if (requestRef.current) {
      cancelAnimationFrame(requestRef.current)
    }
    requestRef.current = requestAnimationFrame(animate);
    return () = > cancelAnimationFrame(requestRef.current)
  }, [duration])
Copy the code

The last

These are some of the things I’ve learned using requestAnimationFrame to implement the countdown. I hope you’ll find it helpful!

More articles: “No more confusion! This article will give you a silky experience with Vue3.0 development”

“Welcome to the discussion in the comments section. The excavation authorities will draw 100 nuggets in the comments section after project Diggnation. See the event article for details.”