Javaskript tajmeri i asinhroni problemi

Ako malo prelistamo dokumentaciju, možemo naći funkciju setInterval() , koja se koristi za za periodično pozivanje neke druge funkcije. Sa druge strane, postoji funkcija setTimeout(), koja poziva neku drugu funkciju posle određenog vremena.

Primer (setInterval()):

function hello() {
  console.log('Sta radis?');
}

setInterval(hello, 1000);

Na svakih 1000 milisekundi (1 sekunda) će se pozvati funkcija hello(), koja će u konzoli ispisati 'Sta radis?'.

Primer (setTimeout()):

function hello() {
  console.log('Dobro dosao');
}

setTimeout(hello, 1000);

Posle 1000 milisekundi (1 sekunda) će se pozvati funkcija hello(), koja će u konzoli ispisati 'Dobro dosao'.

Iz opisa možemo zaključiti da se setInterval() koristi za događaje koji se periodično ponavljaju, dok se setTimeout()koristi za odlaganje nekog događaja, što je delimično tačno.

Ako setTimeout() poziva funkciju u kojoj je definisan (rekurzija) dobijamo isti efekat koji dobijamo i pomoću setInterval() funkcije.

function hello() {
  console.log('Sta radis?');
  setTimeout(hello(), 1000);
}

Da li ima razlike?

Iako u ovom primeru nema razlike, ona definitivno postoji. Razika će se pokazati u slučaju kada naša funkcija radi nešto vremenski zahtevno.

Ako funkcija, koja se izvršava, radi neki zahtevan posao ili jednostavno radi AJAX poziv, i ima duže vreme izvršavanja nego što je postavljeni interval, dolazi do zagušenja. setInterval() će probati da održi svoj interval, gde dolazimo do problema da se javlja dupli poziv iako ne treba. Taj problem možemo da rešimo ako umesto setInterval() iskoristimo setTimeout(). Ali prvo da vidimo na primeru kako se ponašaju.

let runs = 0;
let execTime = 0;
const startTime = new Date().getTime();

function hello() {
  if (++runs === 10) {
    clearInterval(timer);
  }

  const now = new Date().getTime();
  execTime = now - startTime;

  // Trosi neko vreme neko vreme
  for (let i = 0; i < 100000; i++) {
    document.querySelector('body > a');
  }
                             
  let exec = new Date().getTime() - now;
                             
  console.log(`#${runs} poziv je poceo u ${execTime}ms i trajao je ${exec}ms`);

};

const timer = setInterval(hello, 1000);

Pokretanjem gore navedenog koda, dobijamo sledeći izlaz:

#1 poziv je poceo u 1000ms i trajao je 529ms
#2 poziv je poceo u 2001ms i trajao je 566ms
#3 poziv je poceo u 3000ms i trajao je 554ms
#4 poziv je poceo u 4001ms i trajao je 553ms
#5 poziv je poceo u 5002ms i trajao je 546ms
#6 poziv je poceo u 6001ms i trajao je 640ms
#7 poziv je poceo u 7003ms i trajao je 730ms
#8 poziv je poceo u 8002ms i trajao je 680ms
#9 poziv je poceo u 9003ms i trajao je 607ms
#10 poziv je poceo u 10003ms i trajao je 623ms

Ovde nema nikakvog iznenađenja. Funkciji treba neko vreme da se izvrši ali setInterval() odžava svoj interval.

Ono što možemo da primetimo je da početak poziva funkcije nije toliko precizan (kasni 2-3 ms). To je iz razloga zato što tajmeri u Javaskripti nisu toliko pouzdani. Moguće je da se sinhronizuju sa Date objektom.

Ako zamenimo setInterval() sa setTimeout() dobijamo sledeći slučaj

let runs = 0;
let execTime = 0;
const startTime = new Date().getTime();

function hello() {

  const now = new Date().getTime();
  execTime = now - startTime;

  // Trosi neko vreme neko vreme
  for (var i = 0; i < 100000; i++) {
    document.querySelector('body > a');
  }

  console.log(`#${runs + 1} poziv je poceo u ${execTime}ms i trajao je ${new Date().getTime() - now}ms`);

  if (++runs < 10) {
    setTimeout(hello, 1000);
  }
};

setTimeout(hello, 1000);

Što će nam dati sledeći rezultat:

#1 poziv je poceo u 1002ms i trajao je 565ms
#2 poziv je poceo u 2570ms i trajao je 601ms
#3 poziv je poceo u 4173ms i trajao je 533ms
#4 poziv je poceo u 5709ms i trajao je 509ms
#5 poziv je poceo u 7219ms i trajao je 642ms
#6 poziv je poceo u 8863ms i trajao je 742ms
#7 poziv je poceo u 10607ms i trajao je 848ms
#8 poziv je poceo u 12455ms i trajao je 651ms
#9 poziv je poceo u 14107ms i trajao je 564ms
#10 poziv je poceo u 15673ms i trajao je 673ms

Ovde takođe nema nikakvog iznenađenja. Pošto već znamo da setTimeout() ne prati nikakav interval, već sačeka da se završi funcija onda čeka odrđeno vreme pre sledećeg poziva.

Ok, sve je ovo već poznato,ali koja je reazlika u "realnom" svetu:

Recimo da radimo neki file upload i da se obrada fajla radi na backend serveru. Mi uploadujemo fajl, a zatim moramo konstanto da proveravamo da li je obrada završena. Ako koristimo setInterval() da radimo GET zahtev na svaku sekundu, ono što se može desiti jeste da je server zauzet i ako server ne vrati odgovor u roku od našeg intervala (jedna sekunda), funkcija setInterval() neće mariti — u suštini, ona ni ne zna, time će da pošalje još jedan AJAX zahtev. Dok kod setTimeout() rekurzije, mi prvo sačekamo odgovor i ako treba nakon toga pošaljemo još jedan AJAX zahtev.

Haos

Kada setInterval() preskoči interval šta se desi? Haos!!!

Ono što se desi zavisi malo od brzine računara kao i od veb pregledača koji korisnik koristi. Postoje dve mogućnosti:

  1. Pravi se da ništa nije bilo i normalno nastavi sa intervalom.
  2. Kaže "nema veze" i započne potpuno novi interval, na osnovu onog pogrešnog.

Kao i sve u veb svetu, ponašanje je u zavisnosti od veb pregledača. Firefox je jedinsteven, zato što jedino on ima ovo prvo (1) ponašanje. Doduše ovo nije potpuno tačno, zapravo se ponaša kao kombinacija prvog i drugog, evo kako:
U slučaju da FF propusti samo jedan otkucaj onda će se ponašati na prvi (1) način, u slučaju da propusti više od jednog otkucaja, prebaciće se na drugi (2) način.

Svi ostali veb pregledači imaju drugo (2) ponašanje.

Zaključak

Ako Vam je imalo stalo do tajminga ne koristite setInterval() već iskoristite rekurziju i setTimeout(). Za nijansu je lakše koristiti setInterval() ali može dovoesti do grešaka i raznih problema koji nisu očigledni.

Početkom 2011. Firefox i Chrome su postavili minimalno vreme čekanja za setTimeout i setInterval na jednu sekundu kada se pokreće na kartici koja nije aktivna (u pozadini).