preface
There are a lot of scenarios where timers are used at work, but you’ll find that sometimes timers don’t perform as expected, such as setTimeout(()=>{},0), which doesn’t execute immediately as expected. To understand why this is the case, we first need to understand how Javascript timers work.
Working principle of timer
In order to understand the inner workings of timers, we first need to understand a very important concept: timers are not guaranteed delays. Because all single-threaded asynchronous JavaScript events (such as mouse-click events and timers) that are executed in the browser are executed only when it is free.
This may not be very clear, but let’s look at the picture below
There’s a lot of information to digest in the diagram, but fully understanding it will give youBetter understand asynchronous JavaScript executionHow it works. The diagram is one-dimensional: the vertical direction is time in milliseconds. The blue box represents the part of JavaScript that is executing. For example, the first JavaScript block executes about 18ms, the mouse-click block executes about 11ms, and so on.
Because JavaScript can only execute one piece of code at a time (due to its single-threaded nature), each piece of code “blocks” the process for other asynchronous events. This means that when an asynchronous event occurs (such as a mouse click, a timer firing, or an XMLHttpRequest completion), it will queue up to be executed later.
First, in the first block of JavaScript, two timers are started: a 10ms setTimeout and a 10ms setInterval. Because of where and when the timer is started, it actually fires before we actually finish the first code block, but note that it doesn’t execute immediately (it can’t do that because of the thread). Instead, delayed functions are queued for execution at the next available moment.
Also, in the first JavaScript block, we see the mouse click occur. The JavaScript callback associated with this asynchronous event (we never know when the user will perform an action, so it is considered asynchronous) cannot execute immediately, so, just like the initial timer, it is queued up to execute later.
Immediately after the initial block of JavaScript completes execution, the browser asks the question: What is waiting to be executed? In this case, both the mouse-click handler and the timer callback are waiting. The browser then selects one (mouse click callback) and executes it immediately. The timer will wait until the next possible time for execution.
The setInterval call is deprecated
At the time of the click event, the second setInterval also expires at the 20th millisecond, because the click event has already occupied the thread, so the setInterval still cannot be executed, and because there is already a setInterval queued for execution, So this time the setInterval call will be discarded.
Browsers do not add the same setInterval handler to the queue more than once.
In fact, we can see that the interval itself is executing when the third interval callback is fired. This shows us an important fact: The interval doesn’t care what is currently being executed, they will queue indiscriminately, even if it means that the time between callbacks will be sacrificed.
setTimeout
/setInterval
There is no guarantee that callback functions will be executed on time
Finally, after the second interval callback completes, we can see that the JavaScript engine has nothing left to execute. This means that the browser now waits for a new asynchronous event to occur. When the interval fires again, we get this value at 50ms. But this time, there’s nothing stopping it from executing, so it fires immediately.
OK, in general what makes JS timers unreliable is that JavaScript is single-threaded and can only execute one task at a time, and the second parameter of setTimeout() simply tells JavaScript how long to add the current task to the queue. If the queue is empty, the added code executes immediately; If the queue is not empty, then it will wait for the previous code to finish executing the timer task and must wait for the main thread task to execute before it can start executing, whether or not it reaches the time we set
Here we can look at Javascript event loops again
Event loop
All tasks in JavaScript are divided into synchronous tasks and asynchronous tasks. Synchronous tasks, as the name implies, are immediately executed tasks, which are generally executed directly into the main thread. Our asynchronous task enters the task queue and waits for the main task to complete.
A task queue is a queue of events that indicate that related asynchronous tasks can be placed on the execution stack. The main thread reads the task queue to read what events are in it.
A queue is a first-in, first-out data structure.
As mentioned above, asynchronous tasks can be divided into macro tasks and micro tasks, so task queues can also be divided into macro task queues and micro task queues
-
Macrotask Queue: Perform large tasks, such as setTimeout, setInterval, user interaction, UI rendering, etc.
-
Microtask Queue: perform smaller tasks, such as Promise, process. nextTick;
- Synchronous tasks are placed directly into the main thread for execution, while asynchronous tasks (click events, timers, Ajax, etc.) hang in the background for execution, waiting for I/O events to complete or action events to be triggered.
- The system executes asynchronous tasks in the background. If an asynchronous task event (or behavior event) is triggered, the task is added to the task queue, and each task is processed by a callback function.
- The asynchronous task is divided into macro task and micro task. The macro task enters the macro task queue, and the micro task enters the micro task queue.
- The tasks in the execution task queue are specifically completed in the execution stack. When all the tasks in the main thread are executed, the microtask queue is read. If there are any microtasks, they are all executed, and then the macro task queue is read
- The above process is repeated over and over again, which is often referred to as an event-loop.
See my previous post for more details hereExplore JavaScript execution mechanisms
The timer is unreliable
The current task is executed for a long time. Procedure
The JS engine will execute synchronous code before asynchronous code. If synchronous code execution takes too long, asynchronous code execution will be delayed.
setTimeout(() = > {
console.log(1);
}, 20);
for (let i = 0; i < 90000000; i++) { }
setTimeout(() = > {
console.log(2);
}, 0);
Copy the code
This would have been expected to print a 2 and then a 1, but it didn’t. Even though the second timer was shorter, the for loop in the middle took much longer than either timer.
The setTimeout callback tasks are added to the delay queue in sequence. When a task is completed, ProcessDelayTask calculates the expired tasks based on the time of origination and the delay time, and then executes the expired tasks in sequence.
After executing the previous tasks, both settimeouts in the above example expire, so executing in sequence prints 1 and 2. So in this scenario, setTimeout looks less reliable.
The delay time has the maximum value
Browsers including Internet Explorer, Chrome, Safari, and Firefox store latency internally as a 32-bit signed integer. This causes a delay greater than 2,147,483,647 milliseconds (approximately 24.8 days) to overflow, causing the timer to be executed immediately. (MDN)
When the second parameter of setTimeout is set to 0 (default for unset, less than 0, and greater than 2147483647), it means to execute immediately or as soon as possible.
setTimeout(function () {
console.log("When do you suppose it will print?")},2147483648);
Copy the code
So if you put this code in the browser console and you execute it, you’ll see that it prints immediately and guess when it prints?
Minimum delay >=4ms(nested use timer)
In browsers, setTimeout()/setInterval() has a minimum interval of 4ms per call, usually due to function nesting (up to a certain level of nesting) or due to blocking of setInterval callback functions that have already been executed.
-
When the second parameter of setTimeout is set to 0 (default for unset, less than 0, and greater than 2147483647), it means to execute immediately or as soon as possible.
-
If the delay is less than 0, the delay is set to 0. If the timer is nested for more than five times and the delay is less than 4ms, the delay is set to 4ms.
function cb() { f(); setTimeout(cb, 0); }
setTimeout(cb, 0);
Copy the code
In Chrome and Firefox, the fifth call to the timer is blocked; In Safari it was on the 6th; Edge is in third time. So all the subsequent timers were delayed by at least 4ms
The minimum timing delay for inactive tabs is >=1000ms
In order to optimize the loading consumption of background tabs (and reduce power consumption), the browser limits the timer delay to 1S(1000ms) on inactive tabs.
let num = 100;
function setTime() {
// Timing of the current second execution
console.log('Current number of seconds:The ${new Date().getSeconds()}- Times of execution:The ${100-num}`);
num ? num-- && setTimeout(() = > setTime(), 50) : "";
}
setTime();
Copy the code
Here I cut to the other tabs at 39 seconds, and we’ll see that the subsequent execution interval is 1 second, not 50ms as we set it.
SetInterval Indicates that the processing duration cannot be longer than the specified interval
The processing duration of setInterval cannot be longer than the specified interval; otherwise, setInterval will be executed repeatedly without interval
However, for this problem, in many cases, we cannot clearly control the time consumed by the processing program. In order to trigger the timer periodically at a certain interval, we can use setTimeout instead of setInterval.
setTimeout(function fn(){
// todo
setTimeout(fn,10)
// After executing the contents of the handler, call the program at the end of the 10 millisecond interval, so that the call is guaranteed to be a 10 millisecond interval, where the time is written as desired
},10)
Copy the code
The solution
Method 1: requestAnimationFrame
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 a callback that is executed before the browser’s next redraw, ideally 60 times per second (60fsp) or every 16.7ms, but 16.7ms is not guaranteed.
const t = Date.now()
function mySetTimeout (cb, delay) {
let startTime = Date.now()
loop()
function loop () {
if (Date.now() - startTime >= delay) {
cb();
return;
}
requestAnimationFrame(loop)
}
}
mySetTimeout(() = >console.log('mySetTimeout' ,Date.now()-t),2000) / / 2005
setTimeout(() = >console.log('SetTimeout' ,Date.now()-t),2000) / / 2002
Copy the code
This scenario seems to increase error because requestAnimationFrame executes every 16.7ms, so it is not suitable for timer corrections at very small intervals.
Method 2: Web Worker
Web workers provide an easy way for Web content to run scripts in background threads. Threads can perform tasks without interfering with the user interface. In addition, they can perform I/O using XMLHttpRequest (although the responseXML and Channel properties are always empty). Once created, a worker can send a message to the JavaScript code that created it by publishing the message to the event handler specified by that code (and vice versa).
The function of Web workers 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 some computationally intensive or high-latency tasks are burdened by Worker threads and the main thread is not blocked or slowed down.
// index.js
let count = 0;
// Time-consuming task
setInterval(function(){
let i = 0;
while(i++ < 100000000);
}, 0);
// worker
let worker = new Worker('./worker.js')
Copy the code
// worker.js
let startTime = new Date().getTime();
let count = 0;
setInterval(function(){
count++;
console.log(count + The '-' + (new Date().getTime() - (startTime + count * 1000)));
}, 1000);
Copy the code
This solution experience is generally good, correcting the timer significantly without affecting the main process tasks
conclusion
Due to the single-threaded nature of JS, events are queued, fifO, setInterval calls are discarded, timers are not guaranteed to execute callback functions on time, and setintervals are continuously executed.