preface

Usually for countdown effects, setInterval is used, but this can cause some problems, the most common problem being incorrect timers.

If it is just ordinary animation effect, it doesn’t matter, but the countdown needs to be accurate to the millisecond level, not good, otherwise the activity is over, the user interface countdown is still walking, but can not participate in the activity, will be complained of ╮(╯▽╰ ╭

First, knowledge matting

1. SetInterval timer

First, the main character of this article, setInterval, is explained by MDN Web Doc as follows:

The setInterval() method repeatedly calls a function or executes a code segment, with a fixed time delay between calls.

Return an intervalID. (Can be used to clear timer)

Syntax: let intervalID = window.setInterval(func, delay[, param1, param2… ); Ex. :

Note that if you use this in a setInterval, this refers to a window object and can be changed by calling, applying, etc.

SetTimeout is similar to setInterval except that it delays the execution of the function once for n milliseconds and does not need to be manually cleared.

As for the operation principle of setTimeout and setInterval, there is another concept involved: event loop.

2. Event Loop of the browser

JavaScript generates execution environments during execution, and these execution environments are sequentially added to the execution stack. If asynchronous code is encountered, it is suspended and added to the task queue (there are multiple tasks).

Once the execution stack is empty, the Event Loop takes the code that needs to be executed from the Task queue and places it in the execution stack.

With event loop, JavaScript is capable of asynchronous programming. (But essentially, synchronous behavior)

Let’s start with a classic interview question:

console.log('Script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

new Promise((resolve, reject) => {
  console.log('Promise');
  resolve()
}).then(() => {
  console.log('Promise 1');
}).then(() => {
  console.log('Promise 2');
});

console.log('Scritp end');
Copy the code

Print order:

  1. “Script start”
  2. “Promise”
  3. “Script end”
  4. “Promise 1”
  5. “Promise 2”
  6. “setTimeout”

Why setTimeout is printed at the end when it is set to 0 is a matter of micro and macro tasks in the Event loop.

2.1 Macro and micro tasks

Different task sources will be assigned to different task queues. Task sources can be divided into microtasks and macrotasks.

In the ES6:

  • Microtask called Job
  • Macrotask referred to as a Task

Macro-task (Task): An event loop has one or more task queues. Task task sources are very broad, such as Ajax onload, click events, basically all kinds of events that we often bind are task task sources, and database operations (IndexedDB), Note that setTimeout, setInterval, and setImmediate are also task sources. To sum up, task

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

Micro-task (Job): A Microtask queue is similar to a task queue in that the task is provided by a specified task source. The difference is that an event loop has only one Microtask queue. There are also differences between microtask execution timing and MacroTasks

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver

Ps: Micro tasks are not faster than macro tasks

2.2 Event Loop Execution sequence

  1. Execute synchronized code (macro task);
  2. If the execution stack is empty, check whether there are micro tasks to be executed.
  3. Perform all microtasks;
  4. Render the UI if necessary;
  5. Then start the next event loop, which executes the asynchronous code in the macro task;

Ps: If the asynchronous code in the macro task is computation-intensive and requires DOM manipulation, you can put the operation in the microtask for faster interface response.

If setTimeout(()=>{… }, 0) will have a 4ms delay.

Since JavaScript is single-threaded, the setInterval/setTimeout error cannot be completely resolved.

It could be an event in a callback, or it could be a variety of events in the browser.

This is why a page running for a long time, the timer will not be the reason.

Ii. Project scenario

I met the requirement of countdown in the company’s project, but the components have been written by predecessors. Because the project was in a hurry, I directly used them. However, I found some bugs in the process of using them:

  1. On one Android test machine, when a finger slides or is about to slide, it stops for milliseconds, then releases it and continues.
  2. [Fixed] [Bug Mc-10874] – Wrong number of seconds in countdown after going to another page
  3. After returning to the original page, re-request the data, will cause the countdown speed;

The first is because sliding blocks the main thread, causing MacroTask not to execute properly.

The second Bug is that after switching pages, the browser automatically extends the interval of the previous page timer in order to reduce performance consumption, resulting in an increasing error.

The third Bug is that the timer is not cleared before the method is called, causing the timer to be added when listening for the timestamp.

The first two bugs are the ones that this article addresses.

After checking a lot of articles, the general solutions are as follows:

1. requestAnimationFrame()

The MDN Web Doc is explained as follows:

Tell the browser window. RequestAnimationFrame () – you want to perform an animation, and required the browser until the next redraw calls the specified callback function to update the animation. This method takes as an argument a callback function that is executed before the browser’s next redraw

Note: if you want to in the browser to update until the next redraw the next frame animation, then the callback function itself must once again call window. RequestAnimationFrame ()

RequestAnimationFrame () executes at a rate that depends on the refresh rate of the browser screen, which is usually 60 hz or 75Hz, meaning that it can only be redrawn 60 or 75 times per second at most, The basic idea behind requestAnimationFrame is to synchronize with this refresh rate and use it to redraw the page. In addition, using this API, the page automatically stops refreshing once it is not in the browser’s current TAB. This saves CPU, GPU and power.

Note, however, that requestAnimationFrame is done on the main thread. This means that if the main thread is very busy, the animation of the requestAnimationFrame will be compromised.

RequestAnimationFrame can replace setInterval to some extent, but the interval needs to be calculated. 1000/60 = 16.6666667(ms) at 60Hz screen refresh rate (FPS). That’s about every 16.7ms, but FPS is not fixed, as anyone who has played FPS (first person shooter) will know. But the error is much smaller than setInterval, which didn’t do any optimization before.

The next time I enter the animation function, subtract [then] from [current timestamp] to get the interval, then subtract [interval] from [countdown timestamp], and record the departure time when I leave the page. Further reduce the error.

<script>
export default {
  name: "countdown",
  props: {
    timestamp: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      remainTimestamp: 0
      then: 0}; },activated () {
    window.requestAnimationFrame(this.animation);
  },
  deactivated() {
    this.then = Date.now();
  },
  methods: {
    animation(tms) {
      if(this.remainTimestamp > 0 && this.then) { this.remainTimestamp -= (tms - this.then); This. then = TMS; / / record the time of execution window. RequestAnimationFrame (enclosing animation); } } }, watch: { timestamp(val) { this.remainTimestamp = val; this.then = Date.now(); window.requestAnimationFrame(this.animation); }}}; </script>Copy the code

RequestAnimationFrame is used differently from setInterval, but the main difference is that the interval cannot be customized.

If the countdown only needs to be accurate to the second, executing 16.7 times in 1000ms is a bit of a waste of performance. To emulate setInterval, extra variables are needed to handle the interval, reducing the readability of the code.

Therefore, I continue to try the second solution: Web Worker.

2. Web Worker

Web Worker is a black technology of JavaScript to achieve multi-threading, explained in Ruan Yifeng’s blog as follows:

The JavaScript language uses a single-threaded model, which means that all tasks can only be done on one thread, one thing at a time. The first task is not finished, the next task has to wait. With the enhancement of computer computing ability, especially the emergence of multi-core CPU, single thread brings great inconvenience and can not give full play to the computing ability of the computer. The function of Web Worker is to create a multithreaded environment for JavaScript, allowing the main thread to create Worker threads and assign some tasks to the latter to run. While the main thread is running, the Worker thread is running in the background without interfering with each other. Wait until the Worker thread completes the calculation and returns the result to the main thread. The advantage of this is that when computationally intensive or high-latency tasks are taken on by Worker threads, the main thread (usually responsible for UI interactions) will flow smoothly and will not be blocked or slowed down. Once a Worker thread is created, it is always running and is not interrupted by activity on the main thread, such as a user clicking a button or submitting a form. This facilitates communication that responds to the main thread at any time. However, this also causes the Worker to consume resources, so it should not be overused and should be closed once it is used.

See Nguyen yifeng’s blog and MDN – Using Web Workers for more details.

However, using Web workers in Vue projects can be tricky.

The first is file loading. The official example looks like this:

var myWorker = new Worker('worker.js');

Since the Worker cannot read local files, the script must come from the network. If the download does not succeed (such as a 404 error), the Worker silently fails.

Therefore, we can not import directly, otherwise we will not find the file, so Google found two solutions;

2.1 the vue – worker

This is a plug-in for the Vue project written by the authors of the Simply-Web-worker that can invoke functions like Promise.

Github address: vue-worker

SetInterval does not execute:

Val is passed in as the timestamp remaining in the countdown, but the return val is not changed, so setInterval is not executed. In theory, Web workers keep setInterval. (Could it be my posture? Went to mention issues, still no one reply now, have big guy to advise?

SetInterval, the core of the countdown, cannot be executed, so discard this plugin and execute Plan B.

2.2 the worker – loader

Babel-loader is a JavaScript file escape plugin similar to babel-loader.

How to use Web Workers in ES6+Webpack

Post code directly:

timer.worker.js:

self.onmessage = function(e) {
  let time = e.data.value;
  const timer = setInterval(() => {
    time -= 71;
    if(time > 0) {
      self.postMessage({
        value: time
      });
    } else{ clearInterval(timer); self.postMessage({ value: 0 }); self.close(); }}}, 71);Copy the code

countdown.vue:

<script>
import Worker from './timer.worker.js'
export default {
  name: "countdown",
  props: {
    timestamp: {
      type: Number,
      default: 0
    }
  },
  data() {
    return {
      remainTimestamp: 0
    };
  },
  beforeDestroy () {
    this.worker = null;
  },
  methods: {
    setTimer(val) {
      this.worker = new Worker();
      this.worker.postMessage({
        value: val
      });
      const that = this;
      this.worker.onmessage = function(e) { that.remainTimestamp = e.data.value; } } }, watch: { timestamp(val) { this.worker = null; this.setTimer(val); }}}; </script>Copy the code

There is a small incident here, there is no problem when running locally, but there is an error when packing. Check the reason is that the rules of worker-loader are written after the babel-loader, and the result is the.js file that matches first. Worker. js is processed by babel-loader, and worker cannot be imported successfully.

Webpack.base.conf.js (the company project is old, we don’t use webPack 4.0+ configuration, but the principle is the same)

  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          vueLoaderConfig,
          postcss: [
            require('autoprefixer')({
              browsers: ['last 10 Chrome versions'.'last 5 Firefox versions'.'Safari >= 6'.'ie > 8'})]}}, {// The match must be written in front, otherwise the package will report an errortest: /\.worker\.js$/,
        loader: 'worker-loader',
        include: resolve('src'),
        options: {
          inline: true// Inline worker as a BLOB fallback:false, // Disable chunk name:'[name]:[hash:8].js'}}, {test: /\.js$/,
        loader: 'babel-loader',
        include: [utils.resolve('src'), utils.resolve('test')]}, / /... ] },Copy the code

Third, summary

After some trouble, I have deepened my understanding of the event loop of the browser. Not only timer tasks such as setInterval, but other highly intensive calculations can also be processed by multi-threading. However, it is important to close the thread after processing, otherwise it will seriously consume resources. However, normal animation should be done with requestAnimationFrame or CSS animation to improve the smoothness of the page as much as possible.

It is the first time to write a technical blog, so there are inevitably omissions. If there are better countdown solutions, welcome your advice.

References:

  1. Browser event loop mechanism
  2. Web Worker Tutorial – Ruan Yifeng
  3. Official worker-loader document
  4. How to use Web Workers in ES6+Webpack