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:
- Tworzona jest zmienna
data
(1) i funkcjaloadData()
(3) - Odpalana jest funkcja
loadData()
(10). W jej wnętrzu wywoływana jest funkcjasetTimeout()
(5), która odkłada na bok przekazaną do niej funkcję strzałkową - Wykonywany jest
console.log(data)
(10) - 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:
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