Żyjąc w pośpiechu chcemy, aby wszystko co nas otacza dotrzymywało nam tempa. Kurier nie dostarczył paczki, a weekend tuż tuż? Kiepsko. Współbiegacz jest kilka ulic za nami? Okropieństwo. SetInterval zacina się, choć powinien działać błyskawicznie? Tragedia!
Zachwyt i rozczarowanie.
Mamy dwie możliwości wykonywania cyklicznych operacji. Albo używamy setInterval (ewentualnie setTimeout w pętli) albo requestAnimationFrame. I wszystko jest ok dopóki nasz skrypt wykorzystujący jedną z opcji działa w aktywnej zakładce. W momencie kiedy przełączymy się na inną zakładkę, skrypt zostaje wstrzymany albo zwalnia. A ściśle mówiąc, jeśli mamy do czynienia z requestAnimationFrame, to metoda zostanie spauzowana w większości przeglądarek, a w przypadku setInterval czy setTimeout, interwał poniżej 1s zostanie "zaokrąglony" do 1s. Chcesz się upewnić? Proszę bardzo:
let counter = 1;
const startTime = Date.now();
const timer = setInterval(function() {
if (counter === 20) {
clearInterval(timer);
document.querySelector("i").innerText = (Date.now() - startTime) / 1000;
}
document.querySelector("span").innerText = counter++;
}, 500);
W tym krótkiem skrypcie odliczamy co pół sekundy do 20. Wartość licznika umieszczona jest w elemencie SPAN. 20-tkę licznik powinien więc osiągnąć w 10 sekund. I tak jest, kiedy zakładka jest aktywna. Zresztą czas działania zobaczysz w elemencie I. Klawo!
Spróbuj jednak uruchomić skrypt i natychmiast przełączyć się na inną zakładkę. Co zobaczysz po 10 sekundach kiedy wrócisz do zakładki? Skrypt nadal odlicza. A kiedy skończy? Skończy po 20 sekundach. Sprawdź to jeszcze raz uruchamiając skrypt, przełączając się na inną zakładkę, tym razem odczekując osławione 20 sekund. Smutek!
Jak cieszyć się w pełni setInterval?
Z pomocą przyjdzie nam robol zwany Worker'em. Pozwala on uruchomić skrypt JS jako odrębny wątek. I pomimo przełączenia się na inną zakładkę nasz Worker wykonuje skrypt pełną parą. Skoro tak, to trzeba przenieść setInterval właśnie do niego. Ale jak Worker skomunikuje się z głównym skryptem i da znać przy każdym wywołaniu co określony czas? Od tego mamy metodę postMessage, która pozwala wysyłać informacje z głównego lub wątku Workera oraz zdarzenie onMessage, które pozwala je odbierać. W naszym przypadku nie musimy wysyłać konkretnej informacji, może być pusta, po to tylko aby wywołać zdarzenie onMessage w głównym wątku, które będzie nam symulować setInterval (który byłby standardowo umieszczony właśnie w tym wątku).
Worker będzie wyglądać tak:
setInterval(function() {
postMessage("");
}, 500);
a kod głównego wątku:
let counter = 1;
const startTime = Date.now();
/* inicjujemy Worker */
const worker = new Worker("setinterval-licznik-worker.js");
/* czekamy na wiadomość zwrotną
z postMessage wysyłaną przez
kod Worker'a
*/
worker.addEventListener("message", function() {
if (counter === 20) {
/* przerywamy działanie Worker'a */
worker.terminate();
document.querySelector("i").innerText =
(Date.now() - startTime) / 1000;
}
document.querySelector("span").innerText = counter++;
});
Tym razem, niezależnie od tego czy zakładka jest aktywna czy nie, powinieneś otrzymać zbliżony do 10s czas wykonania skryptu.
Przydatne linki:
Web Workers
Obcinanie interwału setInterval
Licznik z wpisu oparty o setInterval
Licznik z wpisu oparty o Worker