Můžeme se rozhodnout, že funkci nespustíme právě teď, ale až za nějakou dobu. To se nazývá „časování volání“ („scheduling a call“).
K tomu existují dvě metody:
setTimeoutnám umožňuje spustit funkci jednou po uplynutí zadaného časového intervalu.setIntervalnám umožňuje spouštět funkci opakovaně, nejprve po uplynutí zadaného časového intervalu a pak bude neustále v tomto intervalu opakována.
Tyto metody nejsou součástí specifikace JavaScriptu, ale většina prostředí obsahuje interní plánovač a tyto metody poskytuje. Konkrétně jsou podporovány ve všech prohlížečích a v Node.js.
setTimeout
Syntaxe:
let idČasovače = setTimeout(funkce|kód, [prodleva], [arg1], [arg2], ...)
Parametry:
funkce|kód- Funkce nebo řetězec kódu, který se má spustit. Obvykle to bývá funkce. Z historických důvodů lze předat i řetězec kódu, ale to se nedoporučuje.
prodleva- Prodleva před spuštěním v milisekundách (1000 ms = 1 sekunda), standardně 0.
arg1,arg2…- Argumenty této funkce.
Například tento kód zavolá řekniAhoj() po uplynutí jedné sekundy:
function řekniAhoj() {
alert('Ahoj');
}
setTimeout(řekniAhoj, 1000);
S argumenty:
function řekniAhoj(věta, kdo) {
alert( věta + ', ' + kdo );
}
setTimeout(řekniAhoj, 1000, "Ahoj", "Jan"); // Ahoj, Jan
Je-li první argument řetězec, pak z něj JavaScript vytvoří funkci.
Bude tedy fungovat i tohle:
setTimeout("alert('Ahoj')", 1000);
Používání řetězců se však nedoporučuje, místo nich používejte šipkové funkce, například takto:
setTimeout(() => alert('Ahoj'), 1000);
Začínající vývojáři někdy dělají chybu v tom, že za funkci přidají závorky ():
// špatně!
setTimeout(řekniAhoj(), 1000);
To nefunguje, protože setTimeout očekává odkaz na funkci. A zde řekniAhoj() zavolá funkci a teprve výsledek jejího volání se předá funkci setTimeout. V našem případě výsledek řekniAhoj() je undefined (funkce nic nevrací), takže se nic nenačasuje.
Zrušení pomocí clearTimeout
Volání setTimeout vrátí „identifikátor časovače“ idČasovače, který můžeme použít ke zrušení načasovaného spuštění.
Syntaxe zrušení:
let idČasovače = setTimeout(...);
clearTimeout(idČasovače);
V následujícím kódu načasujeme funkci a pak ji zrušíme (rozmysleli jsme si to). Výsledkem bude, že se nic nestane:
let idČasovače = setTimeout(() => alert("tohle se nikdy nestane"), 1000);
alert(idČasovače); // identifikátor časovače
clearTimeout(idČasovače);
alert(idČasovače); // stejný identifikátor (po zrušení se nevynuloval)
Jak vidíme z výstupu alert, v prohlížeči je identifikátorem časovače číslo. V jiných prostředích to může být něco jiného, například Node.js vrací objekt časovače s dalšími metodami.
Opakujeme, že pro tyto metody neexistuje žádná univerzální specifikace, takže je to v pořádku.
Pro prohlížeče jsou časovače popsány v kapitole o časovačích HTML Living Standardu.
setInterval
Metoda setInterval má stejnou syntaxi jako setTimeout:
let idČasovače = setInterval(funkce|kód, [prodleva], [arg1], [arg2], ...)
Všechny argumenty mají stejný význam. Na rozdíl od setTimeout však funkce nebude spuštěna jenom jednou, ale pravidelně vždy po uplynutí zadaného časového intervalu.
Chceme-li zastavit další volání, měli bychom zavolat clearInterval(idČasovače).
Následující příklad zobrazí zprávu každé 2 sekundy. Po 5 sekundách bude výstup zastaven:
// opakovat s intervalem 2 sekundy
let idČasovače = setInterval(() => alert('tik'), 2000);
// konec po 5 sekundách
setTimeout(() => { clearInterval(idČasovače); alert('stop'); }, 5000);
alert, čas plyne dálVe většině prohlížečů včetně Chrome a Firefoxu v době, kdy je zobrazeno alert/confirm/prompt, vnitřní časovač „tiká“ dál.
Když si tedy spustíte výše uvedený kód a okno alert nějakou dobu nezrušíte, pak bude následující alert zobrazen hned, jakmile to uděláte. Skutečný interval mezi zobrazeními tedy bude kratší než 2 sekundy.
Vnořený setTimeout
Existují dva způsoby, jak něco pravidelně spouštět.
První je setInterval. Druhý je vnořený setTimeout, například takto:
/** namísto:
let idČasovače = setInterval(() => alert('tik'), 2000);
*/
let idČasovače = setTimeout(function tik() {
alert('tik');
idČasovače = setTimeout(tik, 2000); // (*)
}, 2000);
Uvedený setTimeout načasuje další volání na konci aktuálního na řádku (*).
Vnořený setTimeout je flexibilnější metoda než setInterval. Tímto způsobem můžeme příští volání načasovat za jinou dobu, podle výsledků aktuálního volání.
Například potřebujeme napsat službu, která každých 5 sekund pošle na server žádost o data, ale v případě, že je server přetížen, by tento interval měla zvýšit na 10, 20, 40 sekund…
Zde je pseudokód:
let prodleva = 5000;
let idČasovače = setTimeout(function požadavek() {
...pošle požadavek...
if (požadavek neuspěl kvůli přetížení serveru) {
// zvýšíme interval dalšího spuštění
prodleva *= 2;
}
idČasovače = setTimeout(požadavek, prodleva);
}, prodleva);
A jestliže funkce, které časujeme, zatěžují CPU, můžeme měřit dobu, jakou trvá jejich spuštění, a naplánovat si další volání na dříve nebo později.
Vnořený setTimeout nám umožňuje nastavovat prodlevu mezi voláními přesněji než setInterval.
Porovnejme si dva fragmenty kódu. První používá setInterval:
let i = 1;
setInterval(function() {
funkce(i++);
}, 100);
Druhý používá vnořený setTimeout:
let i = 1;
setTimeout(function spusť() {
funkce(i++);
setTimeout(spusť, 100);
}, 100);
Pro setInterval vnitřní plánovač spustí funkce(i++) každých 100 ms:
Všimli jste si?
Skutečná prodleva mezi voláními funkce pro setInterval je nižší, než uvedená v kódu!
To je normální, protože doba, kterou zabere výkon funkce, „spotřebuje“ část intervalu.
Může se stát, že výkon funkce bude trvat déle, než jsme očekávali, a vyžádá si více než 100 ms.
V takovém případě motor počká, až se funkce dokončí, pak zkontroluje plánovač, a pokud jeho čas vypršel, spustí funkci okamžitě znovu.
V krajním případě, jestliže výkon funkce trvá vždy déle než prodleva ms, bude k voláním docházet úplně bez prodlevy.
A zde je obrázek pro vnořený setTimeout:
Vnořený setTimeout zaručuje pevnou prodlevu (zde 100 ms) před koncem jednoho volání a začátkem následujícího.
Je to proto, že nové volání je naplánováno na konci předchozího.
Když je do setInterval/setTimeout předána funkce, vytvoří se na ni interní odkaz a ten se uloží do plánovače. To brání sběrači odpadků, aby funkci odstranil, i když na ni nejsou žádné jiné odkazy.
// funkce zůstane v paměti, dokud ji plánovač nezavolá
setTimeout(function() {...}, 100);
Pro setInterval funkce zůstane v paměti, dokud není zavolán clearInterval.
To má vedlejší efekt. Funkce se odkazuje na vnější lexikální prostředí, takže dokud existuje, existují i vnější proměnné. Ty mohou zabrat mnohem více paměti než samotná funkce. Když tedy načasovanou funkci už nepotřebujeme, je lepší ji zrušit, i kdyby byla velmi malá.
setTimeout s nulovou prodlevou
Existuje speciální způsob použití: setTimeout(funkce, 0) nebo jen setTimeout(funkce).
To načasuje výkon funkce na co nejdříve. Plánovač ji však spustí až poté, co bude dokončen aktuálně běžící skript.
Funkce je tedy načasována ke spuštění „hned po“ aktuálním skriptu.
Například tohle vypíše „Ahoj“ a hned pak „světe“:
setTimeout(() => alert("světe"));
alert("Ahoj");
První řádek „uloží volání do kalendáře za 0 ms“. Avšak plánovač „zkontroluje kalendář“ až poté, co bude aktuální skript dokončen, takže "Ahoj" půjde jako první a "světe" až po něm.
V prohlížečích existují i pokročilejší způsoby použití načasování s nulovou prodlevou, které probereme v kapitole Smyčka událostí: mikroúlohy a makroúlohy.
V prohlížeči je omezení, jak často se mohou vnořené časovače spouštět. HTML Living Standard říká: „po pěti vnořených časovačích musí být interval alespoň 4 milisekundy.“
Na následujícím příkladu si předvedeme, co to znamená. Volání setTimeout v něm načasuje samo sebe s nulovou prodlevou. Každé volání si pamatuje v poli časy skutečný čas, který uplynul od předchozího. Jak budou vypadat skutečné prodlevy? Podívejme se:
let začátek = Date.now();
let časy = [];
setTimeout(function spusť() {
časy.push(Date.now() - začátek); // zapamatujeme si prodlevu od předchozího volání
if (začátek + 100 < Date.now()) alert(časy); // za 100 ms zobrazíme prodlevy
else setTimeout(spusť); // jinak znovu načasujeme
});
// příklad výstupu:
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
Nejprve se časovače spustí okamžitě (tak, jak je uvedeno ve specifikaci) a pak vidíme 9, 15, 20, 24.... Povinná prodleva aspoň 4 ms mezi voláními vstupuje do hry.
Podobná věc se stane, jestliže použijeme setInterval namísto setTimeout: setInterval(f) spustí f několikrát s nulovou prodlevou a poté s prodlevou nejméně 4 ms.
Toto omezení pochází ze starých časů a mnoho skriptů se na ně spoléhá, takže existuje z historických důvodů.
Pro JavaScript na straně serveru toto omezení neexistuje a jsou tam jiné způsoby, jak načasovat okamžitou asynchronní činnost, například setImmediate pro Node.js. Tato poznámka je tedy specifická pro prohlížeče.
Shrnutí
- Metody
setTimeout(funkce, prodleva, ...argumenty)asetInterval(funkce, prodleva, ...argumenty)nám umožňují spustit funkcifunkcejednou/pravidelně poprodlevamilisekundách. - Chceme-li spuštění zrušit, měli bychom volat
clearTimeout/clearIntervals hodnotou, kterou vrátila funkcesetTimeout/setInterval. - Flexibilnější alternativou k
setIntervaljsou vnořená volánísetTimeout, která nám umožňují nastavit čas mezi voláními přesněji. - Nulová prodleva v
setTimeout(funkce, 0)(totéž jakosetTimeout(funkce)) se používá k načasování volání „co nejdříve, ale až po dokončení aktuálního skriptu“. - Prohlížeč omezuje minimální prodlevu pro pět nebo více vnořených volání
setTimeoutnebo prosetInterval(po 5. volání) na 4 ms. Je tomu tak z historických důvodů.
Prosíme všimněte si, že žádná časovací metoda nezaručuje přesnou prodlevu.
Například časovač v prohlížeči může zpomalit z mnoha důvodů:
- CPU je přetížen.
- Záložka prohlížeče běží na pozadí.
- Laptop je v režimu úspory baterie.
To všechno může zvýšit minimální rozlišení časovače (minimální prodlevu) na 300 ms nebo dokonce 1000 ms v závislosti na prohlížeči a nastavení výkonu na úrovni OS.
Komentáře
<code>, pro několik řádků je obalte značkou<pre>, pro více než 10 řádků vložte odkaz na pískoviště (plnkr, jsbin, codepen…)