JavaScript timers: all you need to know

Original author: Samer Buna
  • Transfer
Hello colleagues. A long time ago, an article by H. Rezig was written on Habré on this subject. 10 years have passed, and the topic still needs clarification. Therefore, we suggest those interested in reading the article by Samer Buna, which not only provides a theoretical overview of the timers in JavaScript (in the context of Node.js), but also the tasks for them.




A few weeks ago, I tweeted the following question from a single interview:

“Where is the source code for the setTimeout and setInterval functions? Where would you look for it? You can not google :) "

*** Answer it for yourself, and then read on ***



About half of the responses to this tweet were incorrect. No, the case is NOT CONNECTED with V8 (or other VM) !!! Functions like setTimeoutand setInterval, proudly referred to as "JavaScript Timers", are not included in any ECMAScript specification or implementation of the JavaScript engine. Timer functions are implemented at the browser level, so their implementation differs in different browsers. Also, timers are natively implemented in the Node.js runtime itself.

In browsers, the main function-timers relate to the interface Window, also associated with some other functions and objects. This interface provides global access to all its elements in the main JavaScript scope. That is why the function setTimeoutcan be performed directly in the browser console.

Node timers are part of the object. globalwhich is arranged like a browser interface Window. The source code for the timers in Node is shown here .

It may seem to someone that this is just a bad question from an interview - what is the use of knowing this? I, as a JavaScript developer, think so: it is assumed that you should know this, because the opposite may indicate that you do not quite understand how V8 (and other virtual machines) interact with browsers and Node.

Consider a few examples and solve a couple of tasks on timers, let's go?

To run the examples in this article, you can use the node command. Most of the examples reviewed here appear in my Getting Started with Node.js course at Pluralsight.

Deferred function

Timers are higher-order functions with which you can postpone or repeat the execution of other functions (the timer receives such a function as the first argument).

Here is an example of deferred execution:

// example1.js
setTimeout(
  () => {
    console.log('Hello after 4 seconds');
  },
  4 * 1000
);

In this example, the setTimeoutoutput of the greeting message is delayed by 4 seconds. The second argument setTimeoutis the delay (in ms). I multiply 4 by 1000 to get 4 seconds.

The first argument setTimeoutis a function whose execution will be postponed.
If you execute the file example1.jswith the node command, the Node pauses for 4 seconds and then displays a welcome message (after which the output will follow).

Note: the first argument setTimeoutis just a function reference . It should not be a built-in function - such as example1.js. Here is the same example without using the built-in function:

const func = () => {
  console.log('Hello after 4 seconds');
};
setTimeout(func, 4 * 1000);

Passing Arguments

If the function for which delay is used setTimeoutaccepts any arguments, you can use the remaining arguments of the function itself setTimeout(after those 2 that we have already studied) to transfer the values ​​of the arguments to the deferred function.

// Для: func(arg1, arg2, arg3, ...)// Можно использовать: setTimeout(func, delay, arg1, arg2, arg3, ...)

Here is an example:

// example2.jsconst rocks = who => {
  console.log(who + ' rocks');
};
setTimeout(rocks, 2 * 1000, 'Node.js');

The above function rocks, deferred for 2 seconds, takes an argument who, and the call setTimeoutpasses it the value of “Node.js” as such an argument who.

When the example2.jscommand executes the nodephrase “Node.js rocks” will be displayed in 2 seconds.

The task for timers # 1.

So, based on the material we have already studied about setTimeout, we derive 2 following messages after the corresponding delays.

  • The message “Hello after 4 seconds” is displayed after 4 seconds.
  • The message “Hello after 8 seconds” is displayed after 8 seconds.

Restriction

In your solution, you can define only one function containing built-in functions. This means that multiple calls setTimeoutwill need to use the same function.

Solution

This is how I would solve this problem:

// solution1.jsconst theOneFunc = delay => {
  console.log('Hello after ' + delay + ' seconds');
};
setTimeout(theOneFunc, 4 * 1000, 4);
setTimeout(theOneFunc, 8 * 1000, 8);

I theOneFuncget an argument delayand uses the value of this argument delayin the message displayed on the screen. Thus, the function can display different messages depending on the delay value we will tell it.

Then I used theOneFunctwo calls setTimeout, and the first call works after 4 seconds, and the second after 8 seconds. Both of these calls setTimeoutalso get the 3rd argument representing the argument delayfor theOneFunc.

Having executed the file solution1.jswith the node command, we will display the task requirements; moreover, the first message will appear after 4 seconds, and the second after 8 seconds.

Repeat the execution of the function.

And what if I asked you to display a message every 4 seconds, indefinitely?
Of course, you can enclose it setTimeoutin a cycle, but the Timer API also offers a function setIntervalwith which you can program the “eternal” execution of some operation.

Here is an example setInterval:

// example3.js
setInterval(
  () =>console.log('Hello every 3 seconds'),
  3000
);

This code will display a message every 3 seconds. If executed with a example3.jscommand node, Node will output this command until you forcibly terminate the process (CTRL + C).

Canceling Timers

Because an action is assigned when you call a timer function, this action can also be canceled before it is executed.

The call setTimeoutreturns the timer ID, and you can use this timer ID when calling clearTimeoutto cancel the timer. Here is an example:

// example4.jsconst timerId = setTimeout(
  () =>console.log('You will not see this one!'),
  0
);
clearTimeout(timerId);

This simple timer should fire after 0 ms (that is, immediately), but this will not happen, because we capture the value timerIdand immediately cancel the timer with a call clearTimeout.

When executed example4.jsby a command node, Node will not print anything - the process will simply end immediately.

By the way, in Node.js there is another way to set it setTimeoutwith a value of 0 ms. The Node.js Timers API has another function called setImmediate, and it basically does the same thing as setTimeoutwith 0 ms, but in this case, the delay can be omitted:

setImmediate(
  () =>console.log('I am equivalent to setTimeout with 0 ms'),
);

This feature setImmediateis not supported in all browsers . Do not use it in the client code.

Along with clearTimeoutthere is a function clearIntervalthat does the same thing, but with calls setInerval, and there is also a call clearImmediate.

Timer delay is not a guaranteed thing.

You noticed that in the previous example, when performing an operation with setTimeoutafter 0 ms, this operation does not occur immediately (after setTimeout), but only after all the script code (including the call clearTimeout) has been completely executed ?

Let me explain this point with an example. Here is a simple call setTimeoutthat should work out in half a second - but this does not happen:

// example5.js
setTimeout(
  () =>console.log('Hello after 0.5 seconds. MAYBE!'),
  500,
);
for (let i = 0; i < 1e10; i++) {
  // Синхронно блокируем операции
}

Immediately after determining the timer in this example, we synchronously block the runtime environment with a large loop for. The value 1e10is 1 with 10 zeros, so the cycle lasts 10 billion processor cycles (in principle, this is how the overloaded processor is simulated). Node can do nothing until this loop ends.

Of course, in practice, doing so is very bad, but this example helps to understand that the delay setTimeoutis not a guaranteed, but rather a minimum value . A value of 500 ms means that the delay will last at least 500 ms. In fact, the script will take much more time to display the welcome line on the screen. First, he will have to wait until the blocking cycle is completed.

Task for timers # 2

Write a script that will display the message “Hello World” once a second, but only 5 times. After 5 iterations, the script should display the message “Done”, after which the Node process will end.

Restriction : in solving this problem can not be called setTimeout.

Hint : need a counter.

Solution

This is how I would solve this problem:

let counter = 0;
const intervalId = setInterval(() => {
  console.log('Hello World');
  counter += 1;
if (counter === 5) {
    console.log('Done');
    clearInterval(intervalId);
  }
}, 1000);

counterI set the initial value to 0, and then called setInterval, taking its id.

The deferred function will display a message and each time increase the counter by one. Inside the deferred function, we have an if statement that will check if 5 iterations have passed. After 5 iterations, the program displays “Done” and clears the interval value using the captured constant intervalId. The delay interval is 1000 ms.

"Who" exactly causes deferred functions?

When using the JavaScript keyword thisinside a normal function, like this:

functionwhoCalledMe() {
  console.log('Caller is', this);
}

the value in the keyword this will match the caller . If you define the above function inside the Node REPL, then the object will call it global. If you define a function in the browser console, then the object will call it window.

Let's define a function as a property of an object so that it becomes a little clearer:

const obj = { 
  id: '42',
  whoCalledMe() {
    console.log('Caller is', this);
  }
};
// Теперь ссылка на функцию такова: obj.whoCallMe

Now, when dealing with a function, obj.whoCallMewe will directly use the link to it, the object will be the caller obj(identifiable in its own way id):



And now the question is: who will be the caller if you send the link to the obj.whoCallMecall setTimetout?

// Какой текст будет выведен в данном случае??
setTimeout(obj.whoCalledMe, 0);

Who is the caller in this case?

The answer will differ depending on where the timer function is performed. In this case, the dependence on who is the caller is simply unacceptable. You will lose control of the caller, since it is up to the implementation of the timer that will determine who in this case calls your function. If you test this code in a Node REPL, then the caller will be the object Timeout:



Note: this is important only if the JavaScript keyword is this used inside normal functions. When using switch functions, the caller should not bother you at all.

Timer Challenge # 3

Write a script that will continuously display the “Hello World” message with varying delays. Start with a one-second delay, then increase it by one second at each iteration. On the second iteration, the delay will be 2 seconds. In the third - three, and so on.

Include a delay in the displayed message. You should get something like this: Constraints : variables can only be defined with const. Using let or var is impossible. Solution Since the duration of the delay in this task is variable, you cannot use it here, but you can manually adjust the interval execution with the help of a recursive call. The first function executed with will create the next timer, and so on.

Hello World. 1
Hello World. 2
Hello World. 3
...






setIntervalsetTimeoutsetTimeout

In addition, since you cannot use let/ var, we cannot have a counter to increment the delay on each recursive call; instead, you can use the arguments of the recursive function to increment during a recursive call.

Here's how to solve this problem:

const greeting = delay =>
  setTimeout(() => {
    console.log('Hello World. ' + delay);
    greeting(delay + 1);
  }, delay * 1000);
greeting(1);

Task for timers # 4

Write a script that will display the message “Hello World” with the same delay structure as in task # 3, but this time in groups of 5 messages, and there will be a main delay interval in the groups. For the first group of 5 messages, we select an initial delay of 100 ms, for the next one - 200 ms, for the third one - 300 ms, and so on.

Here’s how this script should work:

  • At a mark of 100 ms, the script displays the “Hello World” for the first time, and does so 5 times with an interval increasing by 100 ms. The first message will appear after 100 ms, the second after 200 ms, etc.
  • After the first 5 messages, the script should increase the main delay by as much as 200 ms. Thus, the 6th message will be output in 500 ms + 200 ms (700 ms), the 7th message - 900 ms, the 8th message - in 1100 ms, and so on.
  • After 10 messages, the script should increase the main delay interval by 300 ms. The 11th message should be output after 500 ms + 1000 ms + 300 ms (18000 ms). The 12th message should be output after 2100 ms, etc.

According to this principle, the program should work indefinitely.

Include a delay in the displayed message. You should get something like this (without comments): Restrictions : Only calls (and not ) and only ONE instructions can be used . Solution Since we can only work with calls , here we will need to use recursion, as well as increase the delay of the next call . In addition, we will need the instruction to make this happen only after 5 calls to this recursive function. Here is a possible solution:

Hello World. 100 // При 100 мс
Hello World. 100 // При 200 мс
Hello World. 100 // При 300 мс
Hello World. 100 // При 400 мс
Hello World. 100 // При 500 мс
Hello World. 200 // При 700 мс
Hello World. 200 // При 900 мс
Hello World. 200 // При 1100 мс
...


setIntervalsetTimeoutif



setIntervalsetIntervalif



let lastIntervalId, counter = 5;
const greeting = delay => {
  if (counter === 5) {
    clearInterval(lastIntervalId);
    lastIntervalId = setInterval(() => {
      console.log('Hello World. ', delay);
      greeting(delay + 100);
    }, delay);
    counter = 0;
  }
counter += 1;
};
greeting(100);

Thanks to everyone who read it.

Also popular now: