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.

Jeżeli dokładnie przeczytałeś poprzedni rozdział, od razu powinieneś wiedzieć, czemu powyższy kod nie zadziałał. Kod synchroniczny (np. pętla for, zwykła funkcja, instrukcje if, itp.) wykonuje się w jednym momencie. Nie ma znaczenia, czy pętla wykonuje się sto, czy milion razy. Kod taki będzie wykonywał się linia po linii w tym samym momencie (ewentualnie użytkownik zobaczy to na stronie jako "przycięcie" gdy operacje będą dla silnika zbyt wymagające).

Funkcja setTimeout jest funkcją asynchroniczną. Po uruchomieniu funkcja przekazana w jej nawiasach (setTimeout(() => { ... })) zostanie odłożona na bok, gdzie sobie poczeka na moment, w którym zostanie uruchomiona (w powyższym przypadku po upływie 1 sek). Tak odłożone funkcje odpalane są po zakończeniu wykonywania aktualnego kodu synchronicznego.

W naszym przypadku kolejność kodu będzie więc następująca:

  1. Tworzona jest zmienna data (1) i funkcja loadData() (3)
  2. Odpalana jest funkcja loadData() (10). W jej wnętrzu wywoływana jest funkcja setTimeout() (5), która odkłada na bok przekazaną do niej funkcję strzałkową
  3. Wykonywany jest console.log(data) (10)
  4. Wykonywana jest odłożona funkcja strzałkowa, która podstawia dane pod zmienną data

Problemem powyższego kodu jest więc to, że linijka 11 wykonuje się zbyt wcześnie. Moglibyśmy oczywiście pójść na łatwiznę, i zwyczajnie przenieść ją do wnętrza funkcji za linię 6, ale to głupie rozwiązanie. Gdyby nasza aplikacja zajmowała kilka ekranów kodu, nie mądrym było by by wszystko pisać w jednej dużej funkcji setTimeout.

Jak więc prawidłowo poczekać na zakończenie działania kodu asynchronicznego? Możemy tutaj użyć kilku technik, które poznamy w kolejnych rozdziałach.

Pierwszej z nich używaliśmy już wiele razy. Ba - użyliśmy jej nawet w powyższym kodzie...

Funkcje zwrotne

Pierwszym z rozwiązań 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(t, fn) {
    setTimeout(() => {
        fn("Prawidłowe dane");
    }, t);
}

loadData(1000, 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 getUser(cb) {
    setTimeout(() => {
        const data = { name: 'Karol', age: 20 };
        console.log(data);
        cb(data);
    }, 1000);
}

function readUserStatus(data, cb) {
    setTimeout(() => {
        const processedData = Object.assign({}, data, { status: 'active' });
        console.log(processData);
        cb(processedData);
    }, 1000);
}

function readUserBooks(data, cb) {
    setTimeout(() => {
        const processedData = Object.assign({}, data, { book1: 'Thorgal', book2: 'Tytus' });
        console.log(processedData);
        cb(processedData);
    }, 1000);
}

function displayData(data) {
    setTimeout(() => {
        console.warn(data);
    }, 1000);
}

getUser(function(data) {
    readUserStatus(data, (processedData) => {
        readUserBooks(processedData, (resultData) => {
            displayData(resultData);
        })
    });
});

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
Grafika wygenerowana na stronie https://reibitto.github.io/hadoukenify/

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.

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania

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.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.