Callback to funkcja, którą przekazujesz innej funkcji
Funkcje w JavaScript są wartościami. Możesz przechowywać je w zmiennych, wkładać do tablic i, co tu najważniejsze, przekazywać jako argumenty. Gdy przekazujesz funkcję do innej funkcji, żeby ta mogła ją później wywołać, przekazana funkcja jest callbackiem.
greet nie wie i nie obchodzi jej, co robi formatter. Po prostu wywołuje go z imieniem i używa wyniku. Zachowanie wybierasz, przekazując różne callbacki. Ta elastyczność to cały powód istnienia callbacków.
Callbacki synchroniczne działają od razu
Nie każdy callback jest asynchroniczny. Wiele metod tablic, których już używasz, opiera się na callbackach i wywołuje je synchronicznie, zanim zewnętrzne wywołanie się zakończy:
map, filter i reduce przyjmują callback i wywołują go raz dla każdego elementu, od razu. Zanim map zwróci wynik, wszystkie wywołania callbacka już się odbyły. Nic nie czeka w kolejce na później.
To zwykły wzorzec funkcji wyższego rzędu: "oto praca, oto jak ją zrobić, daj mi wynik". Event loop nie bierze w tym udziału.
Callbacki asynchroniczne działają później
Gdy ludzie mówią "callbacki", zwykle mają na myśli te asynchroniczne. Przekazujesz funkcję do API, które potrzebuje czasu (timer, żądanie sieciowe, odczyt pliku), a API wywołuje twoją funkcję, gdy praca się zakończy.
Kolejność wyjścia: przed, po, a sekundę później timer odpalony. setTimeout nie wstrzymuje programu. Przekazuje callback do środowiska uruchomieniowego, od razu zwraca sterowanie, a reszta skryptu działa dalej. Sekundę później event loop podejmuje callback i go uruchamia.
Schemat "zwróć teraz, oddzwoń później" to model myślowy dla każdego asynchronicznego API opartego na callbackach w JavaScript, od addEventListener po starsze API plików w Node.js.
Konwencja error-first (Node.js)
Zanim pojawiły się obietnice, Node.js ustandaryzował konkretny kształt callbacka: pierwszy argument to błąd (albo null), a kolejne to właściwy wynik. Nadal spotkasz go w starszym kodzie i niektórych bibliotekach.
Kod wywołujący najpierw sprawdza err i wychodzi wcześniej, jeśli ma wartość truthy. Dopiero wtedy ufa wynikowi. To konwencja, której język nie wymusza, ale gdy raz zobaczysz sygnaturę (err, result) => ..., rozpoznasz ją wszędzie.
Callback hell
Kłopoty zaczynają się, gdy jeden krok asynchroniczny zależy od wyniku innego. Każdy callback musi być zagnieżdżony w poprzednim, a kod układa się w schodki uciekające w bok:
To słynna "piramida zagłady", czyli callback hell. Kilka rzeczy sprawia, że jest bolesna:
- Przepływ sterowania skacze zygzakiem zamiast czytać się z góry na dół.
- Każdy poziom powtarza ten sam szablonowy kod
if (err) return .... - Wyjątek rzucony w jednym callbacku nie przechodzi do zewnętrznych: błędy trzeba obsługiwać na każdej warstwie.
- Refaktoryzacja oznacza ponowne wcięcie całego bloku.
Można to trochę spłaszczyć, wydzielając nazwane funkcje, ale główny problem (składanie operacji asynchronicznych z gołych callbacków jest niewygodne) nie znika. To właśnie problem, do którego rozwiązania zaprojektowano obietnice.
Dwie pułapki, które warto znać
Nie wywołuj callbacka przez przypadek. Przekazując callback, przekazujesz samą funkcję, a nie wynik jej wywołania.
Uważaj na this. Jeśli callback to zwykła funkcja używająca this, wartość this zależy od tego, jak callback zostanie wywołany, a nie od miejsca, w którym go zdefiniowano. Funkcje strzałkowe omijają ten problem, bo dziedziczą this z otaczającego zakresu:
Właśnie z tego powodu funkcje strzałkowe są domyślnym wyborem dla callbacków pisanych w miejscu użycia.
Callbacki vs obietnice
Callbacki nadal pojawiają się w synchronicznych API (map, forEach, sort), w event listenerach (element.addEventListener("click", ...)) i w niskopoziomowych hookach środowiska. Przy pracy asynchronicznej, która daje jeden wynik, ekosystem niemal całkowicie przeszedł na obietnice.
Szybkie porównanie:
- Callbacki: bezpośrednie, minimalne, ale źle się składają. Obsługa błędów jest ręczna na każdym kroku.
- Obietnice: wartość reprezentująca przyszły wynik. Łączysz je w łańcuch przez
.then(), błędy obsługujesz raz przez.catch(), a piramida się spłaszcza.
Nadal musisz rozumieć callbacki: na nich zbudowane są obietnice i są wszędzie w kodzie sterowanym zdarzeniami. Ale nowe asynchroniczne API rzadko pisze się dziś na gołych callbackach.
Dalej: obietnice
Obietnice biorą pomysł "zrób to, gdy tamto będzie gotowe" i opakowują go w obiekt, który możesz przekazywać, łączyć w łańcuchy i składać. To temat następnej strony i pomost do async/await, czyli sposobu, w jaki większość nowoczesnego JavaScriptu obsługuje pracę asynchroniczną.
Najczęściej zadawane pytania
Czym jest funkcja callback w JavaScript?
Callback to funkcja, którą przekazujesz jako argument do innej funkcji, żeby ta mogła ją później wywołać. setTimeout(() => console.log('hi'), 1000) przekazuje funkcję strzałkową jako callback: setTimeout ją zapamiętuje i wywołuje, gdy timer się odpali. Callbacki to pierwotny sposób, w jaki JavaScript obsługiwał schemat 'zrób to, gdy tamto będzie gotowe'.
Czym różnią się callbacki synchroniczne od asynchronicznych?
Callback synchroniczny wykonuje się od razu, w trakcie wywołania, które go otrzymało: [1, 2, 3].map(x => x * 2) wywołuje callback trzy razy, zanim map zwróci wynik. Callback asynchroniczny zostaje zapamiętany i wywołany później, gdy wydarzy się jakieś zdarzenie: tak działają setTimeout, fs.readFile i event listenery DOM. Callbacki asynchroniczne nie blokują reszty kodu.
Czym jest callback hell i jak go uniknąć?
Callback hell to kształt piramidy, który powstaje, gdy asynchroniczne callbacki zależą od siebie i lądują zagnieżdżone na kilku poziomach. Przez to przepływ sterowania i obsługa błędów stają się trudne do śledzenia. Rozwiązaniem są obietnice z łańcuchami .then(), a jeszcze lepiej async/await: oba podejścia spłaszczają piramidę z powrotem do czytelnej postaci.