JavaScript-timere: Alt hvad du behøver at vide
For et par uger siden tweetede jeg dette interviewspørgsmål:
*** Svar på spørgsmålet i dit hoved nu, før du fortsætter ***
Omkring halvdelen af svarene på Tweet var forkert. Svaret er IKKE V8 (eller andre VM'er) !! Mens de er kendt som “JavaScript-timere”, fungerer funktioner som setTimeout
og setInterval
er ikke en del af ECMAScript-specifikationerne eller nogen implementeringer af JavaScript-motorer. Timerfunktioner implementeres af browsere, og deres implementeringer vil være forskellige blandt forskellige browsere. Timere implementeres også indbygget af selve Node.js-runtime.
I browsere er de vigtigste timerfunktioner en del af Window
grænsefladen, som har et par andre funktioner og objekter. Denne grænseflade gør alle dens elementer tilgængelige globalt i JavaScript-hovedområdet. Dette er grunden til, at du kan udføre setTimeout
direkte i din browsers konsol.
I Node er timere en del af global
objektet, som opfører sig på samme måde som browserens Window
interface. Du kan se kildekoden til timere i Node her.
Nogle synes måske, det er et dårligt interviewspørgsmål - hvorfor betyder det at kende dette alligevel ?! Som JavaScript-udvikler tror jeg, at du forventes at vide dette, hvis det ikke gør det, kan det være et tegn på, at du ikke helt forstår, hvordan V8 (og andre virtuelle computere) interagerer med browsere og node.
Lad os lave et par eksempler og udfordringer omkring timerfunktioner, skal vi?
Opdatering: Denne artikel er nu en del af min “Komplet introduktion til Node.js”.Du kan læse den opdaterede version heraf.
Forsinkelse af udførelsen af en funktion
Timerfunktioner er højere ordensfunktioner, der kan bruges til at forsinke eller gentage udførelsen af andre funktioner (som de modtager som deres første argument).
Her er et eksempel på forsinkelse:
// example1.js setTimeout( () => { console.log('Hello after 4 seconds'); }, 4 * 1000 );
Dette eksempel bruges setTimeout
til at forsinke udskrivningen af hilsen med 4 sekunder. Det andet argument til setTimeout
er forsinkelsen (i ms). Derfor gangede jeg 4 med 1000 for at gøre det til 4 sekunder.
Det første argument til setTimeout
er den funktion, hvis udførelse bliver forsinket.
Hvis du udfører example1.js
filen med node
kommandoen, holder Node pause i 4 sekunder, og derefter udskriver den hilsen (og afslut derefter).
Bemærk, at det første argument til setTimeout
er bare en funktion henvisning . Det behøver ikke at være en inline-funktion som hvad der example1.js
har. Her er det samme eksempel uden at bruge en inline-funktion:
const func = () => { console.log('Hello after 4 seconds'); }; setTimeout(func, 4 * 1000);
Bestået argumenter
Hvis den funktion, der bruger setTimeout
til at forsinke udførelsen accepterer argumenter, kan vi bruge de resterende argumenter til setTimeout
sig selv (efter de 2, vi hidtil har lært om) til at videresende argumentværdierne til den forsinkede funktion.
// For: func(arg1, arg2, arg3, ...) // We can use: setTimeout(func, delay, arg1, arg2, arg3, ...)
Her er et eksempel:
// example2.js const rocks = who => { console.log(who + ' rocks'); }; setTimeout(rocks, 2 * 1000, 'Node.js');
Den rocks
funktion ovenfor, som er forsinket med 2 sekunder, accepterer et who
argument og setTimeout
opkaldet relæer værdien ” node.js ” som who
argument.
Udførelse example2.js
med node
kommandoen udskriver " Node.js-klipper " efter 2 sekunder.
Timers udfordring nr. 1
Brug det, du hidtil har lært om setTimeout
, at udskrive følgende 2 meddelelser efter deres tilsvarende forsinkelser.
- Udskriv meddelelsen “ Hej efter 4 sekunder ” efter 4 sekunder
- Udskriv meddelelsen “ Hej efter 8 sekunder ” efter 8 sekunder.
Begrænsninger :
Du kan kun definere en enkelt funktion i din løsning, som inkluderer integrerede funktioner. Dette betyder, at mange setTimeout
opkald bliver nødt til at bruge nøjagtig den samme funktion.
Opløsning
Sådan løser jeg denne udfordring:
// solution1.js const theOneFunc = delay => { console.log('Hello after ' + delay + ' seconds'); }; setTimeout(theOneFunc, 4 * 1000, 4); setTimeout(theOneFunc, 8 * 1000, 8);
Jeg har theOneFunc
modtaget et delay
argument og brugt værdien af dette delay
argument i den udskrevne besked. På denne måde kan funktionen udskrive forskellige meddelelser baseret på den forsinkelsesværdi, vi sender til den.
Derefter brugte jeg theOneFunc
i to setTimeout
opkald, et der affyrer efter 4 sekunder og et andet, der affyrer efter 8 sekunder. Begge disse setTimeout
opkald får også et 3. argument til at repræsentere delay
argumentet for theOneFunc
.
Udførelse af solution1.js
filen med node
kommandoen udskriver udfordringskravene, den første besked efter 4 sekunder og den anden besked efter 8 sekunder.
Gentagelse af udførelsen af en funktion
Hvad hvis jeg bad dig om at udskrive en besked hvert 4. sekund for evigt?
Mens du kan sætte setTimeout
en løkke, tilbyder timers API også setInterval
funktionen, hvilket ville opfylde kravet om at gøre noget for evigt.
Her er et eksempel på setInterval:
// example3.js setInterval( () => console.log('Hello every 3 seconds'), 3000 );
Dette eksempel udskriver sin meddelelse hvert 3. sekund. Udførelse example3.js
med node
kommandoen får Node til at udskrive denne meddelelse for evigt, indtil du dræber processen (med CTRL + C ).
Annullering af timere
Fordi opkald til en timerfunktion planlægger en handling, kan denne handling også annulleres, før den bliver udført.
Et opkald til at setTimeout
returnere et timer-ID, og du kan bruge dette timer-ID sammen med et clearTimeout
opkald til at annullere denne timer. Her er et eksempel:
// example4.js const timerId = setTimeout( () => console.log('You will not see this one!'), 0 ); clearTimeout(timerId);
Denne enkle timer antages at affyre efter 0
ms (hvilket gør det øjeblikkeligt), men det gør det ikke, fordi vi indfanger timerId
værdien og annullerer den lige efter med et clearTimeout
opkald.
Når vi udfører example4.js
med node
kommandoen, udskriver Node ikke noget, og processen afsluttes bare.
By the way, in Node.js, there is another way to do setTimeout
with 0
ms. The Node.js timer API has another function called setImmediate
, and it’s basically the same thing as a setTimeout
with a 0
ms but we don’t have to specify a delay there:
setImmediate( () => console.log('I am equivalent to setTimeout with 0 ms'), );
The setImmediate
function is not available in all browsers. Don’t use it for front-end code.
Just like clearTimeout
, there is also a clearInterval
function, which does the same thing but for setInerval
calls, and there is also a clearImmediate
call as well.
A timer delay is not a guaranteed thing
In the previous example, did you notice how executing something with setTimeout
after 0
ms did not mean execute it right away (after the setTimeout line), but rather execute it right away after everything else in the script (including the clearTimeout call)?
Let me make this point clear with an example. Here’s a simple setTimeout
call that should fire after half a second, but it won’t:
// example5.js setTimeout( () => console.log('Hello after 0.5 seconds. MAYBE!'), 500, ); for (let i = 0; i < 1e10; i++) { // Block Things Synchronously }
Right after defining the timer in this example, we block the runtime synchronously with a big for
loop. The 1e10
is 1
with 10
zeros in front of it, so the loop is a 10
Billion ticks loop (which basically simulates a busy CPU). Node can do nothing while this loop is ticking.
This of course is a very bad thing to do in practice, but it’ll help you here to understand that setTimeout
delay is not a guaranteed thing, but rather a minimum thing. The 500
ms means a minimum delay of 500
ms. In reality, the script will take a lot longer to print its greeting line. It will have to wait on the blocking loop to finish first.
Timers Challenge #2
Write a script to print the message “Hello World” every second, but only 5 times. After 5 times, the script should print the message “Done” and let the Node process exit.
Constraints: You cannot use a setTimeout
call for this challenge.
Hint: You need a counter.
Solution
Here’s how I’d solve this one:
let counter = 0; const intervalId = setInterval(() => { console.log('Hello World'); counter += 1; if (counter === 5) { console.log('Done'); clearInterval(intervalId); } }, 1000);
I initiated a counter
value as 0
and then started a setInterval
call capturing its id.
The delayed function will print the message and increment the counter each time. Inside the delayed function, an if
statement will check if we’re at 5
times by now. If so, it’ll print “Done” and clear the interval using the captured intervalId
constant. The interval delay is 1000
ms.
Who exactly “calls” the delayed functions?
When you use the JavaScript this
keyword inside a regular function, like this:
function whoCalledMe() { console.log('Caller is', this); }
The value inside the this
keyword will represent the caller of the function. If you define the function above inside a Node REPL, the caller will be the global
object. If you define the function inside a browser’s console, the caller will be the window
object.
Let’s define the function as a property on an object to make this a bit more clear:
const obj = { id: '42', whoCalledMe() { console.log('Caller is', this); } }; // The function reference is now: obj.whoCallMe
Now when you call the obj.whoCallMe
function using its reference directly, the caller will be the obj
object (identified by its id):

Now, the question is, what would the caller be if we pass the reference of obj.whoCallMe
to a setTimetout
call?
// What will this print?? setTimeout(obj.whoCalledMe, 0);
Who will the caller be in that case?
The answer is different based on where the timer function is executed. You simply can’t depend on who the caller is in that case. You lose control of the caller because the timer implementation will be the one invoking your function now. If you test it in a Node REPL, you’d get a Timetout
object as the caller:

Note that this only matters if you’re using JavaScript’s this
keyword inside regular functions. You don’t need to worry about the caller at all if you’re using arrow functions.
Timers Challenge #3
Write a script to continuously print the message “Hello World” with varying delays. Start with a delay of 1 second and then increment the delay by 1 second each time. The second time will have a delay of 2 seconds. The third time will have a delay of 3 seconds, and so on.
Include the delay in the printed message. Expected output looks like:
Hello World. 1 Hello World. 2 Hello World. 3 ...
Constraints: You can only use const
to define variables. You can’t use let
or var
.
Solution
Because the delay amount is a variable in this challenge, we can’t use setInterval
here, but we can manually create an interval execution using setTimeout
within a recursive call. The first executed function with setTimeout will create another timer, and so on.
Also, because we can’t use let/var, we can’t have a counter to increment the delay in each recursive call, but we can instead use the recursive function arguments to increment during the recursive call.
Here’s one possible way to solve this challenge:
const greeting = delay => setTimeout(() => { console.log('Hello World. ' + delay); greeting(delay + 1); }, delay * 1000); greeting(1);
Timers Challenge #4
Write a script to continuously print the message “Hello World” with the same varying delays concept as challenge #3, but this time, in groups of 5 messages per main-delay interval. Starting with a delay of 100ms for the first 5 messages, then a delay of 200ms for the next 5 messages, then 300ms, and so on.
Here’s how the script should behave:
- At the 100ms point, the script will start printing “Hello World” and do that 5 times with an interval of 100ms. The 1st message will appear at 100ms, 2nd message at 200ms, and so on.
- After the first 5 messages, the script should increment the main delay to 200ms. So 6th message will be printed at 500ms + 200ms (700ms), 7th message will be printed at 900ms, 8th message will be printed at 1100ms, and so on.
- After 10 messages, the script should increment the main delay to 300ms. So the 11th message should be printed at 500ms + 1000ms + 300ms (18000ms). The 12th message should be printed at 21000ms, and so on.
- Continue the pattern forever.
Include the delay in the printed message. The expected output looks like this (without the comments):
Hello World. 100 // At 100ms Hello World. 100 // At 200ms Hello World. 100 // At 300ms Hello World. 100 // At 400ms Hello World. 100 // At 500ms Hello World. 200 // At 700ms Hello World. 200 // At 900ms Hello World. 200 // At 1100ms ...
Constraints: You can use only setInterval
calls (not setTimeout
) and you can use only ONE if statement.
Solution
Because we can only use setInterval
calls, we’ll need recursion here as well to increment the delay of the next setInterval
call. In addition, we need an if statement to control doing that only after 5 calls of that recursive function.
Here’s one possible solution:
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 for reading.
Hvis du lige er begyndt at lære Node.js, offentliggjorde jeg for nylig et første-trins kursus hos Pluralsight , tjek det ud:
