Asynchroniczność w JavaScript cz. 2: Callbacks
Callbacks
Nie sposób mówić o asynchroniczności w JavaScript, nie poruszając tematu callbacków. Dla tych, którzy już chwilę programują w JS, nie jest to na pewno nic nowego. Niektórzy, którzy dopiero zaczynają swoją przygodę "na froncie" mogą jeszcze nie kojarzyć czym one są. Spiesząc z pomocą, lecimy z kolejnym artykułem!
Z czym to się je?
Callbacki to nic innego jak jeden z mechanizmów asynchroniczności w Javascript. Są to funkcje wywoływane po wykonaniu określonego zadania np. po pobraniu danych z sieci. Callbacki można podzielić na dwa typy: synchroniczne i asynchroniczne.
Jak stworzyć callback?
Popatrzmy na przykład poniżej:
const exampleFunction = (paramFunction) => { const result = paramFunction; console.log(result); // wyrzuci nam wynik paramFunction, czyli 4 } const simpleCalculation = () => { return 2 + 2; } exampleFunction(simpleCalculation());
JavaScript daje możliwość przekazywania funkcji jako parametru. Do exampleFunction przekazaliśmy jako parametr funkcję simpleCalculation, która wykonuje prostą operację dodawania. Wynik tej operacji został zapisany i wyświetlony wewnątrz exampleFunction.
Dla utrwalenia wiadomości, możesz pomyśleć jak dla powyższego kodu wyglądałaby jego stos wywołań.
Podsumowując jednym zdaniem
Utworzona funkcja, niewywołana explicite, ale przekazana do innej funkcji jako argument.
Za prawidłową "obsługę", czyli wywołanie z odpowiednimi parametrami odpowiada funkcja, do której callback został przekazany. To na co jeszcze warto zwrócić uwagę, funkcje jako argument przekazujemy bez użycia nawiasów ().
Podział callbacków
- Callback synchroniczny
Jak wspomniałem wyżej, callbacki dzielą się na dwa typy synchroniczne i asynchroniczne. Zacznijmy od tych pierwszych. Callback synchroniczny jest wykonywany w trakcie wykonania funkcji, do której został przekazany. Czyli w momencie, kiedy dochodzi do wywołania callbacku, funkcja nadrzędna musi poczekać, aż zostanie on wykonany. Dopiero po jego zakończeniu może kontynuować działanie. Przykładem takiego callbacku jest kod, który widzieliście w poprzedniej sekcji. Console.log nie wywoła się, dopóki nie zakończy się działanie paramFunction.
- Callback asynchroniczny
Callback asynchroniczny jest wywołaniu dopiero po zakończeniu wywołania funkcji nadrzędnej, czyli jej kod wykonuje się bez oczekiwania na wynik callbacku. Spójrzmy na przykład:
const exampleFunction = () => { setTimeout(() => console.log("async callback"), 5000); console.log("exampleFunction !"); } exampleFunction();
Po wywołaniu exampleFunction dostaniemy na ekran od razu:
exampleFunction !
a po odczekaniu 5 sekund:
async callback
Jak widać kod exampleFunction wywołał się bez oczekiwania na zakończenie callbacku z setTimeout. Jego wynik wrócił do nas po 5 sekundach.
Callback Hell
Callbacki maja jedną poważna wadę. Kod z ich użyciem bywa trudny utrzymaniu. Z czasem może wystąpić zjawisko zwane callback hell.
Czym jest callback hell?
Callback hell to termin określający złożoną hierarchię funkcji zwrotnych. Pisanie wielu zagnieżdżonych wywołań powoduje powstanie gęstej sieci funkcji zwrotnych. W konsekwencji prowadzi to do trudności w utrzymaniu i debugowaniu kodu. Spójrzmy na przykład:
const doSomething = (callback) => { setTimeout(() => { callback('done'); }, 1000); } const doSomethingAgain = (callback) => { setTimeout(() => { callback('done again'); }, 1000); } const doSomethingAgainAgain = (callback) => { setTimeout(() => { callback('done again again'); }, 1000); } const doSomethingAgainAgainAgain = (callback) => { setTimeout(() => { callback('done again again again'); }, 1000); } const doSomethingAgainAgainAgainAgain = (callback) => { setTimeout(() => { callback('done again again again again'); }, 1000); } doSomething((result) => { // gdy doSomething kończy działanie, zawołaj doSomethingAgain i przekaż callback doSomethingAgain((secondResult) => { // gdy doSomethingAgain kończy działanie, zawołaj doSomethingAgainAgain i przekaż callback doSomethingAgainAgain((thirdResult) => { // gdy doSomethingAgainAgain kończy działanie, zawołaj doSomethingAgainAgainAgain i przekaż callback doSomethingAgainAgainAgain((fourthResult) => { // gdy doSomethingAgainAgainAgain kończy działanie, zawołaj doSomethingAgainAgainAgainAgain i przekaż callback doSomethingAgainAgainAgainAgain((finalResult) => { // gdy doSomethingAgainAgainAgainAgain kończy działanie pokaż wynik console.log(finalResult); }); }); }); }); });
W powyższym przykładzie mamy pięć funkcji, każda jako parametr przyjmuje callback. Callback wołany jest po osiągnięciu czasu przekazanego jako parametr setTimeout w poszczególnych funkcjach. Jak widać, kod jest ciężki w czytaniu i zrozumieniu. Wyobraźmy sobie teraz, że musimy dopisać do niego kolejnych kilka wywołań.
Prawdziwy callback hell!
Jak unikać?
Całe szczęście twórcy języka wyciągnęli do nas pomocną dłoń. Wraz z wejściem standardu ES6 pojawiły się promise’y, które korzystają z zalet callbacków, równocześnie rozwiązując ich bolączki. Nie uprzedzajmy jednak faktów, o tym już w następnym artykule!
Zakończenie
Wywołania zwrotne to temat, który każdy adept frontendowej ścieżki musi znać. Całe szczęście jest to dość prosta koncepcja. Myślę, że po tym artykule ten temat na rozmowie nie będzie już dla Ciebie straszny.
Jeżeli jednak coś dalej jest niejasne, znalazłeś błąd, z czymś się nie zgadzasz, daj nam znać!