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:

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.