Window. setTimeout and window.setInterval are the same when using JavaScript in the browser runtime environment. Yeah, the timing has to do with these two functions. But you also know, or you’ve heard, that these two functions are not timed exactly, right?

But we need precise timing, or we should go to 🔨. This article is about how to be precise!

Hopefully, by the time you’re reading this, you’re familiar with browser event loops and Web workers. You don’t need to know much, just the general idea.

Why not?

In most cases, the reason for the incorrect timer is simple: the JavaScript thread is busy executing other functions, so it doesn’t have time to execute the timer’s callback function.

Put a picture here, the browser event loop, unfamiliar students can refer to!

Not much to say, on the example!

Case 1

setTimeout(() = > console.log('It's time! '), 1000);
const now = Date.now();
while(now + 3000 > Date.now());
Copy the code

In the simplest case, the synchronization code takes 3 seconds to hang up. Execution of the synchronized code is the first macro task, and only after it has completed will any other macro tasks be concerned. In this case, we only care about the setTimeout callback when the while statement finishes executing (which takes 3 seconds).

So, you have to wait at least 3 seconds before you see the log! This is clearly not what we want!

Case 2

Similarly, if the previous setTimeout callback is time-consuming, the next setTimeout callback will be blocked and delayed:

setTimeout(() = > {
console.log('[First timer]: Guys, I'm a thief! ');
const now = Date.now();
while(now + 3000 > Date.now());
}, 0);
setTimeout(() = > console.log('[Second timer]: It's time! '), 1000);
Copy the code

Case 3

A previous macro task can interfere with the execution of the next macro task, and microtasks and requestAnimationFrame in an event loop can also affect the next macro task!

setTimeout(() = > console.log('It's time! '), 1000);
requestAnimationFrame(() = > {
const now = Date.now();
while(now + 3000 > Date.now());
});
Copy the code

RequestAnimationFrame is dry for 3 seconds, causing setTimeout to wait even though it’s already in the macro queue at 1 second.

Case number four

We talked about setTimeout, what about setInterval?

setInterval(() = > console.log('One temperature report per second, and you won't forget when you set the time! '), 1000);
const now = Date.now();
while(now + 3000 > Date.now());
Copy the code

As you can imagine, it only prints after three seconds. SetInterval fires three times during the three seconds that the main thread is blocked, so it adds three tasks to the macro queue.

A temperature per second to fill in the report, set a time will not forget! A temperature per second to fill in the report, set a time will not forget! A temperature per second to fill in the report, set a time will not forget!Copy the code

However, this was not the case, and the console printed only one line. In fact, it can be interpreted this way:

fun();
setInterval(fun, time);
/ / is equivalent to
function _() {
setTimeout(_, time);
fun();
}
Copy the code

SetInterval calls setTimeout repeatedly. If the first second callback is not executed, the next setTimeout will not be called at all, and the next second callback will not be executed!

These four cases can be summed up as: the event loop is a one-way time chain, there is a time-consuming in the middle, the later can only be delayed!

Of course, when we actually write the code, we don’t deliberately block the execution of the program, we definitely want to make each task as small as possible. However, while the page is running, there will always be time-consuming tasks occupying the main thread, so how should we accurately timing?

Web Worker appearance

A single thread will cause the timer to be inaccurate, so open a separate thread for timing.

Web Worker is to open a single thread in the rendering process, which can put some code that may conflict with the main thread in time.

Here’s how to do it:

const blob = new Blob([
'console.log('Web Worker starts '); setInterval(() => { self.postMessage(''); Console. log(' blink timer '); }, 1000); `] and {type: "text/javascript" });
const blobUrl = URL.createObjectURL(blob);
new Worker(blobUrl);
Copy the code

You can run the above code in the browser console first, and you will log once per second. Then, enter the following code:

const now = Date.now();
while(now + 3000 > Date.now());
Copy the code

You will find that the 3-second task does not interfere with the Worker thread’s timer, and the log occurs once per second!

The Worker thread has its own set of event loops, so it plays its own and is not affected by main thread blocking.

There are a few caveats, though.

Note 1

I found that the Worker code should be executed only after the surrounding code is executed when the Worker object is created. In other words, in the example above, the Worker code will still execute 3 seconds later if the two pieces of code are put together:

const blob = new Blob([
'console.log('Web Worker starts '); setInterval(() => { self.postMessage(''); Console. log(' blink timer '); }, 1000); `] and {type: "text/javascript" });
const blobUrl = URL.createObjectURL(blob);
new Worker(blobUrl);
const now = Date.now();
while(now + 3000 > Date.now());
Copy the code

This way, the code in the Worker will only be executed after the while statement has executed. It shows that there will be a log every second after 3 seconds.

Generally, there will be no such blocking code when the Worker object is initialized. The use of Web Worker for timing is to prevent the main thread blocking when the Web page is running after its initialization, leading to the blocking of the timer task.

Note 2

The code that you want to execute every second must be placed in Worker code, not in the main thread. For example, in the following cases, it is impossible to guarantee one log per second:

const blob = new Blob([
'console.log('Web Worker starts '); setInterval(() => { self.postMessage(''); }, 1000); `] and {type: "text/javascript" });
const blobUrl = URL.createObjectURL(blob);
const worker = new Worker(blobUrl);
worker.onmessage = () = > console.log('Blink timer');
Copy the code

Then in the browser console type:

const now = Date.now();
while(now + 3000 > Date.now());
Copy the code

In this case, the main thread is blocked, but the Worker thread is scheduled as usual. After 3 seconds, the macro task queue of the main thread is stacked with 3 interprocess communication tasks from the Worker, so the first three logs will be printed in the third second.

Pay attention to point 3

Timing in the Worker does not guarantee punctuality. If the callback function of the timer takes a long time, it will definitely not work:

const blob = new Blob([
'console.log('Web Worker starts '); setInterval(() => { self.postMessage(''); const now = Date.now(); while(now + 3000 > Date.now()); Console. log(' blink timer '); }, 1000); `] and {type: "text/javascript" });
const blobUrl = URL.createObjectURL(blob);
new Worker(blobUrl);
Copy the code

So I must log every three seconds.

Do you think your computer can handle executing more than a second of code once a second?

So this should not happen…

Thoughts on the main thread

In fact, for relatively complex Web pages, we must pull out the functional modules and put them on other threads.

The main thread basically does one thing: control page rendering. If some time-consuming code is executed in the main thread, it will affect not only the timer mentioned above, but also the other macro and micro tasks, and it will affect the rendering of the page, causing the page to feel stuck.