Funkcje interwałowe

W JS 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 = setInteval(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 = setInteval(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 = setInteval(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 intervał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