If you write a timer in JavaScript that dictates that a piece of code be executed once in a while, your first reaction is to use setInterval.

For example, to print hello every 200 milliseconds, you could write it like this:

setInterval((a)= > {
	console.log('hello')},200)
Copy the code

The code above pushes console.log(‘hello’) to the event queue every 200 milliseconds.

The first argument to setIntreavl is the function to be repeated. The second argument is the interval between the repeated execution, in milliseconds. The return value is of type Number, an ID that uniquely identifies the timer. The timer can be cleared using clearInterval(intervalID).

The defect of the setInterval

Consider the extreme case, if the code inside the timer requires a lot of computation, or DOM manipulation.

This can take a long time, and it is possible that the previous code will be added to the queue before it has finished executing.

If the interval is 100 ms, the code to execute will take 300 ms, as shown below:

The figure shows only the first three executions of the timer.

Start by executing setInterval, and 100 milliseconds later the code to be executed is added to the queue.

At 100 ms, the executing code enters the queue, the queue is idle, and the code in the timer executes.

At 200 milliseconds, the first timer code is still executing. The second timer code is pushed into the event queue, waits for the queue to be idle, and then executes.

At 300 milliseconds, the first timer code is still executing, and the second timer code is waiting at the end of the event queue. Because the timer already has a second code waiting in the queue, this code will not be pushed to the queue and will be ignored.

At 400 ms, the first timer code completes, the queue is idle, the next waiting code executes, and the second timer code starts executing.

Just to be clear, the first code and the second code are not 100 mm apart as expected, but the first code is executed, and the second code is executed immediately. Because the first code hasn’t finished executing, the second code is already waiting in the queue.

As for the ignored third timer code, because at 300 ms, the second timer code is already waiting, and the new code of this timer can only queue when the code without this timer is in the queue, so the third timer will not be added to the queue.

As can be seen from the above, in this extreme case, setIntreval does not fulfill the requirements.

Use setTimeout to implement setInterval

SetTimeout can realize the function of setInterval, and the above situation will not occur.

const repeat = (func, ms) = > {
  setTimeout((a)= > {
    func()
    repeat(func, ms)
  }, ms)
}
Copy the code

The repeat above takes two arguments, func being the function to be executed at the interval, and ms being the number of milliseconds of the interval.

Inside the repeat, setTimeout is used to push the anonymous function containing the func to be executed, and the repeat(func, ms) into the event queue after the specified number of milliseconds ms. When the anonymous function is executed, func will be executed first, and then repeat will be recursively called to simulate setInterval. In the repeat recursively called, setTimeout will be executed, and the next timer code will be pushed to the end of the queue after ms milliseconds.

Notice here that when the next timer code is pushed into the queue, the previous code will have already executed anyway, so there will be no repeat of setInterval.

But this implementation method, can not clear the timer, but also need to be transformed.

Improved setTimeout mode

function Timer() {
  this.timeID = null
  this.func = null
}

Timer.prototype.repeat = function(func, ms) {
  if (this.func === null) {
    this.func = func
  }
  
  // Ensure that a Timer instance can only repeat one func
  if (this.func ! == func) {return
  }
  
  this.timeID = setTimeout((a)= > {
    func()
    this.repeat(func, ms)
  }, ms)
}

Timer.prototype.clear = function() {
  clearTimeout(this.timeID)
}

const a = (a)= > console.log('a')

const b = (a)= > console.log('b')

const timer = new Timer()

timer.repeat(a, 1000)
timer.repeat(b, 1000) // b will not be executed periodically
Copy the code

The above code defines the constructor Timer, which generates an instance with two properties. TimeID is used to store the value returned by setTimeout, which is the ID of the Timer. Func is used to store functions that need to be executed periodically.

Timer’s Prototype defines the repeat method, much the same as the previous repeat function. Just at the beginning, this. TimeID retrieves the ID of the latest timer every time setTimeout is executed when the passed func is saved to this.func. Repeat recursion.

Timer prototype also defines the clear method to clear the Timer.

Notice that the repeat and clear methods are defined with function declarations, not anonymous functions. This is done to ensure that this points to the Timer instance within the method. If the arrow function is used, the this in both methods will refer to the global object Window. See this blog post for more information on this.

Next we define two simple functions, A and b.

A new Timer is assigned to the variable Timer, which executes the repeat method on a and B.

You can see that every second or so, a is executed and the console prints a. B does not periodically repeat the execution. This is because a Timer instance can only execute one function repeatedly. If a and B are the same, then when clear, only the timer ID of the last added duplicate code can be cleared.

If b is repeated, only one more Timer can be instantiated.

conclusion

In some cases, setInterval will cause unpredictable changes in the interval between the two timer codes, or even skip the execution of a timer code. Therefore, setTimeout can be used to realize the function of setInterval.