Asynchroniczny JavaScript: od Callback Hell do Async i Await
Opublikowany: 2022-03-11Jednym z kluczy do napisania udanej aplikacji internetowej jest możliwość wykonywania dziesiątek wywołań AJAX na stronie.
Jest to typowe wyzwanie programowania asynchronicznego, a sposób, w jaki zdecydujesz się radzić sobie z wywołaniami asynchronicznymi, w dużej mierze spowoduje utworzenie lub zerwanie aplikacji, a co za tym idzie, potencjalnie całego uruchomienia.
Synchronizacja zadań asynchronicznych w JavaScript przez bardzo długi czas była poważnym problemem.
To wyzwanie dotyka programistów back-endowych używających Node.js w takim samym stopniu, jak programistów front-endowych używających jakichkolwiek frameworków JavaScript. Programowanie asynchroniczne jest częścią naszej codziennej pracy, ale wyzwanie to często jest traktowane lekko i nie jest brane pod uwagę we właściwym czasie.
Krótka historia asynchronicznego JavaScript
Pierwsze i najprostsze rozwiązanie pojawiło się w postaci funkcji zagnieżdżonych jako wywołań zwrotnych . To rozwiązanie doprowadziło do czegoś, co nazywa się piekłem wywołań zwrotnych , a zbyt wiele aplikacji nadal odczuwa to.
Następnie otrzymaliśmy Obietnice . Ten wzorzec znacznie ułatwił odczytanie kodu, ale był daleki od zasady Don't Repeat Yourself (DRY). Nadal było zbyt wiele przypadków, w których trzeba było powtarzać te same fragmenty kodu, aby prawidłowo zarządzać przepływem aplikacji. Najnowszy dodatek, w postaci instrukcji async/await, sprawił, że kod asynchroniczny w JavaScript jest tak łatwy do czytania i pisania, jak każdy inny fragment kodu.
Przyjrzyjmy się przykładom każdego z tych rozwiązań i zastanówmy się nad ewolucją programowania asynchronicznego w JavaScript.
Aby to zrobić, przeanalizujemy proste zadanie, które wykonuje następujące kroki:
- Sprawdź nazwę użytkownika i hasło użytkownika.
- Uzyskaj role aplikacji dla użytkownika.
- Rejestruj czas dostępu do aplikacji dla użytkownika.
Podejście 1: Callback Hell („Piramida Zagłady”)
Starożytnym rozwiązaniem do synchronizacji tych wywołań było zagnieżdżone wywołania zwrotne. To było przyzwoite podejście do prostych asynchronicznych zadań JavaScript, ale nie dałoby się skalować z powodu problemu zwanego callback hell.
Kod dla trzech prostych zadań wyglądałby mniej więcej tak:
const verifyUser = function(username, password, callback){ dataBase.verifyUser(username, password, (error, userInfo) => { if (error) { callback(error) }else{ dataBase.getRoles(username, (error, roles) => { if (error){ callback(error) }else { dataBase.logAccess(username, (error) => { if (error){ callback(error); }else{ callback(null, userInfo, roles); } }) } }) } }) };
Każda funkcja otrzymuje argument, który jest inną funkcją, która jest wywoływana z parametrem będącym odpowiedzią na poprzednią akcję.
Zbyt wiele osób doświadczy zamrożenia mózgu po przeczytaniu powyższego zdania. Posiadanie aplikacji z setkami podobnych bloków kodu sprawi jeszcze więcej kłopotów osobie utrzymującej kod, nawet jeśli sama go napisała.
Ten przykład staje się jeszcze bardziej skomplikowany, gdy zdasz sobie sprawę, że database.getRoles
to kolejna funkcja, która ma zagnieżdżone wywołania zwrotne.
const getRoles = function (username, callback){ database.connect((connection) => { connection.query('get roles sql', (result) => { callback(null, result); }) }); };
Oprócz posiadania kodu, który jest trudny do utrzymania, zasada DRY nie ma w tym przypadku żadnej wartości. Na przykład obsługa błędów jest powtarzana w każdej funkcji, a główne wywołanie zwrotne jest wywoływane z każdej zagnieżdżonej funkcji.
Bardziej złożone asynchroniczne operacje JavaScript, takie jak pętle przez wywołania asynchroniczne, stanowią jeszcze większe wyzwanie. W rzeczywistości nie ma prostego sposobu na zrobienie tego z wywołaniami zwrotnymi. Właśnie dlatego biblioteki JavaScript Promise, takie jak Bluebird i Q, cieszą się tak dużą popularnością. Zapewniają sposób wykonywania typowych operacji na żądaniach asynchronicznych, których sam język jeszcze nie zapewnia.
Tu właśnie pojawiają się natywne obietnice JavaScript.
Obietnice JavaScript
Obietnice były kolejnym logicznym krokiem w ucieczce z piekła zwrotnego. Ta metoda nie usunęła użycia wywołań zwrotnych, ale uprościła łączenie funkcji w łańcuchy i uprościła kod, czyniąc go znacznie łatwiejszym do odczytania.

Po wprowadzeniu obietnic kod w naszym asynchronicznym przykładzie JavaScript wyglądałby mniej więcej tak:
const verifyUser = function(username, password) { database.verifyUser(username, password) .then(userInfo => dataBase.getRoles(userInfo)) .then(rolesInfo => dataBase.logAccess(rolesInfo)) .then(finalResult => { //do whatever the 'callback' would do }) .catch((err) => { //do whatever the error handler needs }); };
Aby osiągnąć taką prostotę, wszystkie funkcje użyte w przykładzie musiałyby być Promisified . Przyjrzyjmy się, jak metoda getRoles
zostałaby zaktualizowana w celu zwrócenia Promise
:
const getRoles = function (username){ return new Promise((resolve, reject) => { database.connect((connection) => { connection.query('get roles sql', (result) => { resolve(result); }) }); }); };
Zmodyfikowaliśmy metodę tak, aby Promise
z dwoma wywołaniami zwrotnymi, a sama Promise
wykonuje akcje z metody. Teraz resolve
i reject
wywołań zwrotnych będzie mapowane odpowiednio na metody Promise.then
i Promise.catch
.
Możesz zauważyć, że metoda getRoles
nadal jest wewnętrznie podatna na zjawisko piramidy zagłady. Wynika to ze sposobu, w jaki tworzone są metody bazy danych, ponieważ nie zwracają one Promise
. Jeśli nasze metody dostępu do bazy danych również zwróciłyby Promise
, metoda getRoles
wyglądałaby następująco:
const getRoles = new function (userInfo) { return new Promise((resolve, reject) => { database.connect() .then((connection) => connection.query('get roles sql')) .then((result) => resolve(result)) .catch(reject) }); };
Podejście 3: Asynchronizacja/Oczekiwanie
Piramida zagłady została znacznie złagodzona wraz z wprowadzeniem Obietnic. Jednak nadal musieliśmy polegać na wywołaniach zwrotnych, które są przekazywane do metod .then
i .catch
Promise
.
Obietnice utorowały drogę do jednego z najfajniejszych ulepszeń w JavaScript. ECMAScript 2017 wprowadził cukier składniowy do obietnic w JavaScript w postaci instrukcji async
i await
.
Pozwalają nam pisać kod oparty na Promise
tak, jakby był synchroniczny, ale bez blokowania głównego wątku, jak pokazuje poniższy przykład kodu:
const verifyUser = async function(username, password){ try { const userInfo = await dataBase.verifyUser(username, password); const rolesInfo = await dataBase.getRoles(userInfo); const logStatus = await dataBase.logAccess(userInfo); return userInfo; }catch (e){ //handle errors as needed } };
Oczekiwanie na rozwiązanie Promise
jest dozwolone tylko w ramach funkcji async
, co oznacza, że verifyUser
musiał zostać zdefiniowany za pomocą async function
.
Jednak po wprowadzeniu tej małej zmiany możesz await
każdą Promise
bez dodatkowych zmian w innych metodach.
Async — długo oczekiwane rozwiązanie obietnicy
Funkcje asynchroniczne to kolejny logiczny krok w ewolucji programowania asynchronicznego w JavaScript. Sprawią, że Twój kod będzie znacznie czystszy i łatwiejszy w utrzymaniu. Zadeklarowanie funkcji jako async
zapewni, że zawsze zwróci Promise
, więc nie musisz się już o to martwić.
Dlaczego już dziś powinieneś zacząć używać funkcji async
JavaScript?
- Otrzymany kod jest znacznie czystszy.
- Obsługa błędów jest znacznie prostsza i opiera się na
try
/catch
, tak jak w każdym innym kodzie synchronicznym. - Debugowanie jest znacznie prostsze. Ustawienie punktu przerwania wewnątrz bloku
.then
nie spowoduje przeniesienia do następnego.then
, ponieważ przechodzi tylko przez kod synchroniczny. Możesz jednak przechodzić przez wywołaniaawait
tak, jakby były wywołaniami synchronicznymi.