
Mam nadzieję, że słyszałeś już co nieco o “callback hell” oraz wiesz jak go uniknąć? Jeśli tak, to spokojnie możesz już zakończyć czytanie tego artykuły w tym miejscu. Jeśli jednak nie masz pojęcia jak pozbyć się callback hell za pomocą obiektów Promise, to koniecznie przeczytaj ten artykuł, ponieważ zawarłem w nim wszystko co musisz wiedzieć na ten temat!

Jak pozbyć się callback hell za pomocą promisów?
Mam nadzieję, że słyszałeś już co nieco o “callback hell” oraz wiesz jak go uniknąć? Jeśli tak, to spokojnie możesz już zakończyć czytanie tego artykuły w tym miejscu. Jeśli jednak nie masz pojęcia jak pozbyć się callback hell za pomocą obiektów Promise, to koniecznie przeczytaj ten artykuł, ponieważ zawarłem w nim wszystko co musisz wiedzieć na ten temat!
Co to jest callback hell?
Myślę, że zanim przedstawię jak pozbyć się callback hell, warto by było pokazać co to w ogóle jest… A najlepiej jest to zrobić na przykładzie:
function asyncFirst (successCallback, failCallback) {
if (true) {
successCallback();
} else {
failCallback();
}
}
function asyncSecond (successCallback, failCallback) {
if (true) {
successCallback();
} else {
failCallback();
}
}
function asyncThird (successCallback, failCallback) {
if (true) {
successCallback();
} else {
failCallback();
}
}
asyncFirst(() => {
asyncSecond(() => {
asyncThird(() => {
// when all async actions are done...
console.log('all done!');
}, function () {
// handle errors here
});
}, function () {
// handle errors here
});
}, function () {
// handle errors here
});
Jak widzisz, mamy tutaj trzy funkcje “async” nazwane asyncFirst
, asyncSecond
oraz asyncThird
. Każda z nich przyjmuje, jako parametry, po dwie funkcje wywołania zwrotnego. Funkcja successCallback
wywoływana jest w przypadku gdy operacja asynchroniczna zakończy się sukcesem. Natomiast funkcja failCallback
odpalana jest w przypadku wystąpienia błędu.
Po zdefiniowaniu opisanych funkcji następuje ich kaskadowe wywołanie (od linii 25). Funkcja asyncFirst
przyjmuje callbacka, który wywołuje funkcję asyncSecond
. Ta funkcja także przyjmuje callbacka, który z kolei wywołuje funkcję asyncThird
, itd. Dodatkowo, każda z tych funkcji przyjmuje jeszcze funkcję wywołania zwrotnego do obsługi błędu…
Przyznasz, że nie wygląda to dobrze i nie jest zbyt czytelne, prawda? Takie wielokrotne zagnieżdżenie funkcji wywołania zwrotnego nazywamy właśnie callback hell. W takim razie jak pozbyć się callback hell?
Pierwsza myśl: wyciągnijmy callbacki do odrębnych funkcji
Zwykle pierwszą myślą, jaka przychodzi do głowy gdy chcemy usprawnić kod podobny do powyższego to “wyciągnijmy te anonimowe callbacki do osobnych, nazwanych funkcji”. Jest to jakiś sposób… ale czy dobry? Poniżej możesz zobaczyć jak by to wyglądało:
function asyncFirst (successCallback, failCallback) { ... }
function asyncSecond (successCallback, failCallback) { ... }
function asyncThird (successCallback, failCallback) { ... }
function asyncCallbackSuccessFirst() {
asyncSecond(asyncCallbackSuccessSecond, asyncCallbackErrorSecond);
}
function asyncCallbackSuccessSecond() {
asyncThird(asyncCallbackSuccessThird, asyncCallbackErrorThird);
}
function asyncCallbackSuccessThird() {
// when all async actions are done...
console.log('all done!');
}
function asyncCallbackErrorFirst() {
// handle errors here
}
function asyncCallbackErrorSecond() {
// handle errors here
}
function asyncCallbackErrorThird() {
// handle errors here
}
// final call of the cascade
asyncFirst(asyncCallbackSuccessFirst, asyncCallbackErrorFirst);
Mniej więcej tak by to musiało wyglądać, jeśli byśmy zastosowali podejście z wyciąganiem callbaków do nazwanych funkcji. Sporo kodu prawda? A przecież zamieniłem kod naszych przykładowych funkcji “async” na wielokropki żeby go trochę skrócić… Nie ma więc raczej wątpliwości, że to rozwiązanie jest słabe.
Na szczęście jest dużo lepszy sposób na rozwiązanie problemu callback hell…
Zróbmy to lepiej
Domyślasz się już na pewno, że teraz pokażę Ci jak pozbyć się callback hell za pomocą promisów… Mam nadzieję, że jesteś już zaznajomiony z koncepcją obiektów Promise. Jeśli nie, zalecam przeczytanie najpierw mojego wpisu na ten właśnie temat. Co prawda tamten artykuł przedstawiał to zagadnienie na przykładzie jego implementacji w jQuery ale myślę, że jest on wystarczający aby zrozumieć o co chodzi.
Ten post piszę pod koniec roku 2016 dlatego też, w poniższym przykładzie użyję obiektu Promise wbudowanego w język od czasu pojawienia się ECMAScript 6 (wspominałem o tym na blogu).
No dobra… ale wróćmy do tematu i przedstawmy przykład użycia obiektu Promise do usunięcia problemu callback hell:
function asyncFirst () {
return new Promise((resolve, reject) => {
if (true) {
resolve();
} else {
reject();
}
});
}
function asyncSecond () {
return new Promise((resolve, reject) => {
if (true) {
resolve();
} else {
reject();
}
});
}
function asyncThird () {
return new Promise((resolve, reject) => {
if (true) {
resolve();
} else {
reject();
}
});
}
asyncFirst()
.then(asyncSecond)
.then(asyncThird)
.then(() => {
// when all async actions are done...
console.log('all done!');
})
.catch(() => {
// error handling...
});
Objaśnienie
Uważny czytelnik od razu zauważy, że tym razem zupełnie pozbyliśmy się funkcji wywołania zwrotnego ze wszystkich naszych funkcji “async”. Zamiast tego, zmieniła się implementacja tych funkcji - zwracają one teraz obiekt Promise. Spójrz na implementacje funkcji przekazywanych do konstruktorów tego obiektu. Zawierają one wywołania funkcji resolve
oraz reject
w miejsce wywołań callbacków successCallback
oraz errorCallback.
Taka zmiana implementacji umożliwia nam to co dzieje się począwszy od linii 31. Dzięki temu, że nasze funkcje asynchroniczne zwracają obiekty Promise, jesteśmy w stanie wywołać je “łańcuchowo”. Funkcja asyncSecond
wywoła się dopiero, kiedy nastąpi wywołanie funkcji resolve
w funkcji asyncFirst
. Z kolei funkcja asyncThird
czeka aż to samo wydarzy się w funkcji asyncSecond
itd.
Dzięki takiemu podejściu pozbyliśmy się zupełnie zagnieżdżenia wywołań. Zamiast tego wywołania tworzą całkowicie płaski łańcuch. Dodatkowo, teraz wystarczy nam teraz tylko jedna funkcja obsługująca wystąpienie błędu… Osom!
A co jeśli nie mogę zmienić implementacji?
Wszystko jest fajnie, jeśli to nasze własne funkcje powodują powstawanie problemu callback hell. Wówczas wystarczy odpowiednio zmienić ich implementację, tak jak pokazałem powyżej. Nie zawsze jednak mamy taki luksus, na przykład jeśli korzystamy z jakichś zewnętrznych bibliotek… Na szczęście jest na to sposób! Możemy umieścić wywołanie takiej asynchronicznej funkcji, pochodzącej z zewnętrznej biblioteki, w funkcji zwracającej obiekt Promise:
function asyncPromiseWrapper () {
return new Promise((resolve, reject) => {
externalAsyncFunction(data => {
resolve(data);
}, error => {
reject(error);
});
});
}
W powyższym przykładzie funkcja externalAsyncFunction
pochodzi z jakiejś zewnętrznej biblioteki. Przyjmuje ona jako parametry dwie funkcje wywołania zwrotnego. Pierwszy odpowiada za obsługę operacji zakończonej sukcesem, drugi obsługuje błąd. Jak widzisz, owinąłem ją w funkcję asyncPromiseWrapper
, która zwraca obiekt Promise. Funkcja resolve
tego obiektu wywoływana jest w ciele funkcji obsługi “sukcesu”, natomiast reject
w ciele funkcji obsługi błędu. Funkcję asyncPromiseWrapper
można teraz używać w sposób pokazany w poprzednim przykładzie. Problem rozwiązany!
Jak pozbyć się callback hell - podsumowanie
Jak widzisz, rozwiązanie problemu callback hell sprowadza się do pozbywania się funkcji wywołania zwrotnego. Zamiast tego lepiej jest zwracać obiekt Promise. Dzięki temu jesteśmy w stanie kilka zależnych od siebie funkcji wywołać w sposób łańcuchowy. Mam nadzieję, że teraz łatwiej Ci będzie pozbyć się wszystkich zagnieżdżonych callbacków ze swojego kodu…