Funkcje zwrotne

Wyobraź sobie, że mamy funkcję, w której wykonujemy operację, która zajmuje jakiś czas:


let data = null;

function loadData() {
    //to może być setTimeout, wczytywanie danych, czy dowolna inna czasochłonna operacja
    setTimeout(() => {
        data = "Prawidłowe dane";
    }, 1000);
}

loadData();
console.log(data); //null

Odpalamy i... mamy null.

Problem z naszą funkcją jest taki, że asynchroniczne setTimeout zakończy się dopiero po sekundzie, natomiast my w 11 linii wypisujemy data od razu. Nie - return nie rozwiąże sprawy, bo zwracając setTimeout zwrócilibyśmy tylko jego ID.

Jak więc to rozwiązać? Możemy tutaj użyć kilku technik, które poznamy w kolejnych rozdziałach.
Zacznijmy od funkcji zwrotnych.

Funkcje zwrotne

Jednym z rozwiązań powyższej sytuacji jest zastosowanie tak zwanych funkcji zwrotnych.

Do każdej funkcji możemy przekazywać dowolne wartości - w tym także inne funkcje. Z taką sytuacją spotkaliśmy się już wielokrotnie:


//funkcja sort wymaga przekazania naszej własnej funkcji
[3, 1, 2].sort((a, b) => a - b);

//forEach wymaga przekazania naszej funkcji, do której przekaże jakieś dane
[1, 2, 3].forEach(el => console.log(el));

//zdarzenia wymagają przekazania nazwy zdarzenia i naszej własnej funkcji
document.body.addEventListener("click", e => { ...});

Podobnie działanie możemy zrobić w przypadku naszych własnych funkcji:


function test(fn) {
    console.log("--------");
    fn();
    console.log("--------");
}

test(() => {
    console.warn("Jakiś tekst");
});

test(() => {
    console.log("Inny tekst");
});

function random(min, max, cb) {
    const nr = Math.floor(Math.random()*(max-min+1)+min);
    cb(nr);
}

random(10, 20, res => {
    alert(`Losowa liczba to ${res}`);
});

random(10, 20, res => {
    for (let i=0; i<res; i++) {
        console.log(i);
    }
});

Podobne podejście pomoże nam wiec rozwiązać początkowy problem:


function loadData(fn) {
    setTimeout(() => {
        fn("Prawidłowe dane");
    }, 1000);
}

loadData(res => {
    console.log(res);
});

Problematyczne funkcje zwrotne

Powyższa technika jest jak najbardziej prawidłowa, i sprawdzi się w wielu sytuacjach.

Gdybyś przyjrzał się dokumentacji Node.js, zobaczysz, że sporo funkcjonalności korzysta z funkcji zwrotnych.

Nie zawsze będzie jednak najlepszym wyborem.

Wyobraź sobie, że musisz wykonać jakieś operacje asynchroniczne ale dopiero wtedy gdy zakończą się inne. Możesz np. chcieć wczytać dane autoryzacyjne użytkownika, a później wczytać inne informacje. Tworzysz więc kilka funkcji i stosujesz powyższą technikę z callback:


function getFirstData(fn) { ... }
function getAnotherData1(fn) { ... }
function getAnotherData2(fn) { ... }
function getAnotherData3(fn) { ... }

function renderPage() {
    getFirstData(res => {
        getAnotherData1(res => {
            getAnotherData2(res => {
                getAnotherData3(res => {

                });
            });
        });
    });
}

function loadData(fn) {
    setTimeout(() => fn("Wczytane dane"), 1000);
}

function makeStuff(fn) {
    setTimeout(() => fn("Inne dane"), 600);
}

function makeOtherStuff(fn) {
    setTimeout(() => fn("Coś innego"), 2000);
}


loadData(res => {
    console.log("Wczytałem dane:", res);

    makeStuff(res => {
        console.log("Wykonałem jakieś działania:", res);

        makeOtherStuff(res => {
            console.log("Wykonałem inne działania:", res);
        });
    })
})

Przy odpalaniu kolejnych funkcji robi nam się z kodu mała choinka zwana potocznie callback hell. Czasami może to doprowadzić do nieco śmiesznych sytuacji:

callback hell

Kod taki staje się ciężki do późniejszego opanowania i testowania. Dodatkowo problematyczne stają się tutaj inne sytuacje - np. pokazanie widoku gdy obie równoczesne asynchroniczne operacje się zakończą.

Z pomocą śpieszą nam obietnice.

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.

Menu