Funkcje interwałowe

W JavaScript istnieją funkcje, które pozwalają odpalić nasz kod z opóźnieniem czasowym, lub pozwalają odpalać taki kod cyklicznie co jakiś czas.

setTimeout()

Pierwszą z takich funkcji jest setTimeout(fn, time).
Służy ona wywołania z opóźnieniem funkcji przekazanej w pierwszym parametrze. W drugim parametrze podajemy czas w milisekundach po jakim zostanie ta funkcja wywołana.


function myFunc() {
    console.log('Jakiś tekst');
}

setTimeout(myFunc, 1200); //odpali po 1.2s

function myFunc() {
    console.log('Jakiś tekst');
}

setTimeout(function() {
    console.log("z zaskoczenia!");
}, 3 * 1000); //odpali po 3s

Gdy używasz setTimeout funkcja która ma być odpalona odkładana jest na stos, a cała reszta kodu jest wykonywana dalej. Gdy zadany czas minie, twoja funkcja zostanie wywołana:


setTimeout(function() {
    console.log('Trzeci tekst');
}, 2000);

console.log('Pierwszy tekst');
console.log('Drugi tekst');

W powyższym przykładzie wpierw zostaną wypisane "Pierwszy tekst" i "Drugi tekst", a po upływie 2 sekund zostanie wypisany także "Trzeci tekst".

Odłożona na stos funkcja zostanie wykonana "później", czyli po kodzie, który nie został nigdzie odłożony. Tyczy się to nawet funkcji, które są odpalane z zerowym opóźnieniem:

    setTimeout(function() {
        console.log('Drugi');
    }, 0)

    console.log('Pierwszy');
    

Dzieje się tak dzięki działaniu tak zwanego event loop. Dokładniej przedstawił to zagadnienie Philip Roberts w swojej prezentacji.

Żeby przerwać wcześniej zainicjowany setTimeout (ale przed jego wykonaniem) korzystamy z metody clearTimeout() która w parametrze przyjmuje zmienną, pod którą zostało wcześniej podstawione wywołanie setTimeout():


const time = setTimeout(function() {
    console.log('Pełne zaskoczenie');
}, 10000);

clearTimeout(time); //powyższa funkcja nigdy się nie odpali, bo od razu przerwaliśmy setTimeouta

setInterval()

Powyższa funkcja setTimeout wywołuje daną funkcję tylko 1 raz.

Kolejna funkcja którą poznamy to setInterval(fn, time), która działa bardzo podobnie do setTimeout. Kluczową różnicą jest tutaj to, że setInterval będzie odpalać naszą funkcję co jakiś czas cyklicznie:


const time = setInterval(function() {
    console.log('Przykładowy napis');
}, 1000);

Po odpaleniu powyższego kodu w naszej konsoli będzie się co sekundę pojawiać napis "Przykładowy napis" aż do zamknięcia strony, lub zatrzymania takiego interwału.

Żeby zatrzymać taki interwał, skorzystamy z metody clearInterval(), która podobnie do clearTimeout() przyjmuje tylko jeden parametr, który jest zmienną, pod która wcześniej zostało podstawione zadeklarowanie setInterval:


const time = setInterval(function() {
    console.log('Wypiszę się co 1 sekundę');
}, 1000);

clearInterval(time);

Przerwanie intervału możemy też wykonywać z jego wnętrza:


let i = 0;

const time = setInterval(function() {
    i++;
    console.log(i);

    if (i >= 10) {
        clearInterval(time);
    }
}, 1000);

Poniższy przykład pokazuje użycie interwału po kliknięciu na przycisk:


const btn = document.querySelector('#btnTest');

btn.addEventListener('click', function() {
    this.disabled = true;
    let i = 5;

    const time = setInterval(function() {
        i--;
        console.log(i);

        if (i <= 0) {
            console.log('Koniec!');
            clearInterval(time);
        }
    }, 1000);
});

Podobnie jak w forEach w funkcjach setTimeout i setInterval this domyślnie wskazuje na obiekt window, a nie element w którym ta funkcja została wywołana.


const btn = document.querySelector('#btnTest');

btn.addEventListener('click', function() {
    this.disabled = true; //this === button
    let i = 5;

    const time = setInterval(function() {
        console.log(i);
        i--;

        if (i <= 0) {
            this.disabled = false; //błąd! - tutaj this wskazuje na window
            console.log('Koniec');
            clearInterval(time);
        }
    }, 1000);
});

Jeżeli po przerwaniu interwału chcielibyśmy klikniętemu przyciskowi wyłączyć disabled, wtedy funkcję wywołaną w setInterval musielibyśmy przypiąć za pomocą metody bind(), tak by wewnętrzne this wskazywało na przycisk, a nie na window:


const btn = document.querySelector('#btnTest');
btn.addEventListener('click', function() {
    this.disabled = true;
    let i = 5;

    const time = setInterval(function() {
        console.log(i);
        i--;

        if (i <= 0) {
            this.disabled = false;
            console.log('Koniec');
            clearInterval(time);
        }
    }.bind(this), 1000);
});

Innym sposobem jest zastosowanie funkcji strzałkowej.

interwał i długo wywołujący się kod

Funkcja setInterval wywołuje naszą funkcję co zadany czas, nie patrząc na to, czy jej kod się zdąży wykonać czy nie. Wyobraź sobie, że wykonanie kodu naszej funkcji będzie trwać dłużej niż zadany czas interwału. Może to być np. mocno obciążające manipulowanie elementami strony, jakaś obciążająca procesor animacja czy chociażby zapytania do serwera, które nie wiadomo dokładnie kiedy się skończą. W takim przypadku wykonywanie naszej funkcji może zacząć się nakładać:


function longFn() {
    let i = 0;

    //symulujemy bardzo długo wywołujący się kod, który może zając kilka sekund
    //poniższy kod wykonywać się będzie przez 2000ms
    const t = setInterval(function() {
        i++;
        console.log(i)
        if (i >= 200) {
            clearInterval(t);
        }
    }, 100); //100 * 200 === 2000
}

let time = setInterval(longFn, 1000);

Jak widzisz liczby są wypisywane w złej kolejności, bo kolejne działania funkcji się nakładają:

przykład nachodzącego intervalu

Jeżeli zakładamy, że wykonanie kodu naszej funkcji może zająć więcej niż odstępy interwału, prawdopodobnie lepszym wyborem będzie zastosowanie setTimeout, który będzie w danej funkcji wywoływał daną funkcję:


function longFn() {
    //bardzo długo wywołujący się kod, który może zając kilka sekund

    time = setTimeout(longFn, 1000);
}

let time = setTimeout(longFn, 1000);

Poprzedni przykład z użyciem setTimeout():


function longFn() {
    let i = 0;

    const t = setInterval(function() {
        i++;
        console.log(i)
        if (i >= 200) {
            clearInterval(t);
            time = setTimeout(longFn, 1000);
        }
    }, 100);
}

let time = setTimeout(longFn, 1000);

Tym razem wszystko już jest ok:

przykład nachodzącego intervalu

Trening czyni mistrza

Poniżej zamieszczam kilka zadań, które w ramach ćwiczenia możesz wykonać:

  1. Zrób interwał, który będzie wypisywała co 200 milisekund liczby od 1 do 20. Po dojściu do 20 interwał niech się przerwie.
    let counter = 1; let time = setInterval(function() { if (counter > 20) { clearInterval(time); } else { console.log(counter); counter++; } }, 200);
  2. Dodaj na stronę div ze stylowaniem:

    
                div {
                    width: 100px;
                    height: 100px;
                    background: red;
                    transition: 1s;
                }
                

    Napisz skrypt, który za pomocą interwału będzie losowo zmieniał wielkość tego diva oraz dodawał mu losowy kolor. Losowy kolor możesz losować tak jak zostało to pokazane tutaj.

    
                const div = document.querySelector("div");
    
                setInterval(function() {
                    const min = 100;
                    const max = 300;
                    const size = Math.floor(Math.random()*(max-min+1)+min);
                    const color = "#" + Math.random().toString(16).substr(2,6);
    
                    div.style.width = size + "px";
                    div.style.height = size + "px";
                    div.style.backgroundColor = color;
                }, 1000)
                
  3. Stwórz w html pojedynczy button z napisem "Kliknij".
    Napisz skrypt, który po kliknięciu na ten button wstawi za nim tekst "kliknąłeś na przycisk" oraz wyłączy (disabled) button.
    Po 2 sekundach aktywuj przycisk i usuń napis.
    const button = document.querySelector("button"); button.addEventListener("click", function() { if (!this.classList.contains("active")) { this.disabled = true; this.classList.add("active"); const div = document.createElement("div"); div.innerText = "Kliknięto przycisk"; this.parentNode.insertBefore(div, this.nextSibling); setTimeout(function() { div.remove(); this.classList.remove("active"); this.disabled = false; }.bind(this), 2000); } });
  4. Dodaj na stronę 50 checkboxów. Za pomocą interwału zaznaczaj co 100ms po kolei każdy kolejny checkbox (coraz więcej). Gdy dojdziesz do ostatniego zacznij od początku.
    
                let counter = 0;
                const input = document.querySelectorAll('input[type="checkbox"]');
    
                setInterval(function() {
                    if (counter > input.length-1) {
                        counter = 0;
                        for (el of input) {
                            el.checked = false;
                        }
                    }
    
                    input[counter].checked = true;
                    counter++;
                }, 200)