JavaScript timers - naše staré hodiny, bijí čtyři hodiny
Absolutním Cimrmanovým rýmem začínám další ze série článků o Javascriptu. V něm bych chtěl rozebrat pár postřehů při práci s časovači (timery) v JavaScriptu. Ty se používají k lecčemu - při jQuery animacích, zobrazování aktuálního času, periodickém dotazováním serveru atp. Intuitivně jsme vždycky tušili, že jejich časování nemusí být úplně přesné, ale přesto jsme hrubě podcenili význam pro aplikaci, pro kterou je aktuální čas zásadní.
Stáli jsme před relativně jednoduchým problémem. Odpočítávat čas do okamžiku T a vypočítávat slevu v ceně na základě času, který do okamžiku T zbývá. Samozřejmě všechny údaje (ať čas nebo cena) musely být u všech klientů naprosto stejné a musely se měnit každou vteřinu. Tento jednoduchý problém nás ale docela potrápil a proto vznikl tento článek, který by měl zachytit problémy a jejich řešení.
Všechny problémy byly způsobeny dvěma příčinami. Ta první je, že každý počítač klienta má odlišný čas a přes všechny synchronizace, které operační systémy provádí.. Nezanedbatelná část klientů má čas posunutý zásadním způsobem v řádech minut nebo i hodin (a to přesto, že se pohybujeme ve stejném časovém pásmu).
Druhou je nepřesnost časovačů v prohlížečích. Pro ilustraci jsem připravil jednoduchou testovací stránku, kterou si sami můžete spustit v různých prohlížečích. Především v těch, kde je horší implementace JavaScriptu (starší verze prohlížečů nebo Internet Explorer), můžeme během první minuty lehce nasbírat zpoždění i několika vteřin.
Vysvětlení důvodu, proč se tak děje se můžete dočíst v článku Johna Resiga, kde rozebírá princip toho jak časovače v prohlížečích fungují. Především je to dáno tím, že veškeré zpracování probíhá v jednom vlákně, takže v případě, že v danou chvíli prohlížeč vyřizuje jiný kus kódu (např. kód ošetřující kliknutí myši), spuštění časovače musí počkat (respektive se zafrontuje).
Z našich chyb jsme si vzali několik poučení
browser má vždy jiný čas než server
Nikdy nepracujte přímo s new Date() při vypočítávání zbývajícího času. Jediný způsob, jak synchronizovat čas všech klientů, je dostat na klienta aktuální serverový čas (i tak nebudete úplně přesní, protože mezi vložením času do HTTP response a zpracování HTTP response na klientovi nějaký čas - byť minimální - uběhne). Na klientu pak vypočtete odchylku systémového času klienta a systémového času serveru a o tuto odchylku upravujte všechna volání new Date() v logice klienta (nebo ještě lépe toto chování extrahujte do nějaké metody).
Příklad:
var serverTimeDifference;
function setServerTime(serverTime) {
serverTimeDifference = new Date().getTime() - serverTime.getTime();
}
function getActualTime() {
var actualTime = new Date();
actualTime.setTime(actualTime.getTime() - serverTimeDifference);
return actualTime;
}
na periodu časovačů se při výpočtech nelze spolehnout
V logice na klientu nikdy nepočítejte s intervalem, který řídí spouštění aktualizační logiky, ale s aktuálním systémovým časem upraveným o odchylku. Přestože je to lákavé, odpočítávání času by nemělo fungovat tak, že si nastavíte timer na 1 sekundový interval a při zavolání metody z proměnné obsahující počet zbývajících sekund odečtete jedničku. Takto odpočet nebude vůbec přesný.
Řešením je přepsat aktualizační metodu tak, že vždy nanovo vypočte počet zbývajících sekund odečtením cílového času udaného serverem od aktuálního času ošetřeného o odchylku.
Závěrem
Když si ten článek po sobě čtu, tak si říkám: "Otec Fura objevil Ameriku". Je zvláštní jak hodně dokáže být člověk i po těch letech naivní.
Reference
- Článek Johna Resiga osvětlující prinicipy fungování Timerů v JavaScriptu
- Článek narážející na podobný problém a řešení odchylky klientského času od času serveru
- Zajímavé zjištění Johna Resiga o problémech spouštění javascriptových performance testů na platformě MS Windows - trošku off-topic ale velmi zajímavé čtení
Komentáře