JavaScript jest asynchronicznym (nieblokującym) i jednowątkowym językiem programowania, co oznacza, że w danym momencie może być uruchomiony tylko jeden proces.
W językach programowania piekło wywołań zwrotnych ogólnie odnosi się do nieefektywnego sposobu pisania kodu za pomocą wywołań asynchronicznych. Nazywana jest także Piramidą Zagłady.
Piekło wywołania zwrotnego w JavaScript określa się jako sytuację, w której wykonywana jest nadmierna liczba zagnieżdżonych funkcji wywołania zwrotnego. Zmniejsza czytelność kodu i konserwację. Sytuacja wywołania zwrotnego zwykle ma miejsce w przypadku asynchronicznych operacji żądań, takich jak wysyłanie wielu żądań do interfejsu API lub obsługa zdarzeń ze złożonymi zależnościami.
string.replaceall w Javie
Aby lepiej zrozumieć piekło wywołań zwrotnych w JavaScript, najpierw poznaj wywołania zwrotne i pętle zdarzeń w JavaScript.
Wywołania zwrotne w JavaScript
JavaScript traktuje wszystko jako obiekt, na przykład ciągi znaków, tablice i funkcje. Dlatego koncepcja wywołania zwrotnego pozwala nam przekazać funkcję jako argument innej funkcji. Funkcja wywołania zwrotnego zakończy wykonanie jako pierwsza, a funkcja nadrzędna zostanie wykonana później.
Funkcje wywołania zwrotnego są wykonywane asynchronicznie i umożliwiają dalsze działanie kodu bez czekania na zakończenie zadania asynchronicznego. Kiedy łączy się wiele zadań asynchronicznych, a każde zadanie zależy od poprzedniego zadania, struktura kodu staje się skomplikowana.
Rozumiemy zastosowanie i znaczenie wywołań zwrotnych. Załóżmy, że mamy funkcję, która przyjmuje trzy parametry, jeden ciąg znaków i dwie liczby. Chcemy wyników opartych na tekście ciągu z wieloma warunkami.
Rozważ poniższy przykład:
function expectedResult(action, x, y){ if(action === 'add'){ return x+y }else if(action === 'subtract'){ return x-y } } console.log(expectedResult('add',20,10)) console.log(expectedResult('subtract',30,10))
Wyjście:
30 20
Powyższy kod będzie działał dobrze, jednak musimy dodać więcej zadań, aby kod był skalowalny. Liczba instrukcji warunkowych również będzie rosła, co doprowadzi do niechlujnej struktury kodu, którą należy zoptymalizować i czytelność.
Możemy zatem przepisać kod w lepszy sposób w następujący sposób:
function add(x,y){ return x+y } function subtract(x,y){ return x-y } function expectedResult(callBack, x, y){ return callBack(x,y) } console.log(expectedResult(add, 20, 10)) console.log(expectedResult(subtract, 30, 10))
Wyjście:
30 20
Mimo to wynik będzie taki sam. Jednak w powyższym przykładzie zdefiniowaliśmy oddzielne ciało funkcji i przekazaliśmy tę funkcję jako funkcję wywołania zwrotnego do funkcji oczekiwanyResult. Jeśli więc będziemy chcieli rozszerzyć funkcjonalność oczekiwanych wyników tak, aby móc stworzyć kolejny funkcjonujący korpus z inną operacją i wykorzystać go jako funkcję wywołania zwrotnego, ułatwi to zrozumienie i poprawi czytelność kodu.
Istnieją inne różne przykłady wywołań zwrotnych dostępnych w obsługiwanych funkcjach JavaScript. Kilka typowych przykładów to detektory zdarzeń i funkcje tablicowe, takie jak mapowanie, redukcja, filtrowanie itp.
Aby lepiej to zrozumieć, powinniśmy zrozumieć sposób przekazywania wartości i przekazywania referencji w JavaScript.
JavaScript obsługuje dwa typy typów danych, które są prymitywne i nieprymitywne. Pierwotne typy danych to niezdefiniowane, null, string i boolean, których nie można zmienić lub możemy powiedzieć, że są względnie niezmienne; nieprymitywne typy danych to tablice, funkcje i obiekty, które można zmieniać lub modyfikować.
Przekazywanie przez referencję przekazuje adres referencyjny jednostki, tak jak funkcja może być traktowana jako argument. Zatem jeśli wartość w tej funkcji zostanie zmieniona, zmieni się pierwotna wartość, która jest dostępna poza funkcją.
Dla porównania koncepcja przekazywania wartości nie zmienia swojej pierwotnej wartości, która jest dostępna poza treścią funkcji. Zamiast tego skopiuje wartość do dwóch różnych lokalizacji, korzystając z ich pamięci. JavaScript zidentyfikował wszystkie obiekty na podstawie ich referencji.
W JavaScript funkcja addEventListener nasłuchuje zdarzeń takich jak kliknięcie, najechanie myszką i wysunięcie myszy i przyjmuje drugi argument jako funkcję, która zostanie wykonana po wyzwoleniu zdarzenia. Ta funkcja jest używana w koncepcji przekazywania przez odwołanie i przekazywana bez nawiasów.
Rozważ poniższy przykład; w tym przykładzie przekazaliśmy funkcję greet jako argument do funkcji addEventListener jako funkcję wywołania zwrotnego. Zostanie wywołane po wywołaniu zdarzenia kliknięcia:
Test.html:
Javascript Callback Example <h3>Javascript Callback</h3> Click Here to Console const button = document.getElementById('btn'); const greet=()=>{ console.log('Hello, How are you?') } button.addEventListener('click', greet)
Wyjście:
W powyższym przykładzie przekazaliśmy funkcję greet jako argument do funkcji addEventListener jako funkcję wywołania zwrotnego. Zostanie wywołane po wywołaniu zdarzenia kliknięcia.
Podobnie filtr jest również przykładem funkcji wywołania zwrotnego. Jeśli użyjemy filtru do iteracji tablicy, do przetworzenia danych tablicy pobierze inną funkcję wywołania zwrotnego jako argument. Rozważ poniższy przykład; w tym przykładzie używamy funkcji Great do wydrukowania w tablicy liczby większej niż 5. Używamy funkcji isGreater jako funkcji wywołania zwrotnego w metodzie filter.
const arr = [3,10,6,7] const isGreater = num => num > 5 console.log(arr.filter(isGreater))
Wyjście:
[ 10, 6, 7 ]
Powyższy przykład pokazuje, że funkcja większa jest używana jako funkcja wywołania zwrotnego w metodzie filter.
Aby lepiej zrozumieć wywołania zwrotne i pętle zdarzeń w JavaScript, omówmy synchroniczny i asynchroniczny JavaScript:
Synchroniczny JavaScript
Rozumiemy, jakie są cechy synchronicznego języka programowania. Programowanie synchroniczne ma następujące cechy:
Blokowanie wykonania: Synchroniczny język programowania obsługuje technikę blokowania wykonywania, co oznacza, że blokuje wykonanie kolejnych instrukcji, gdyż istniejące instrukcje zostaną wykonane. Dzięki temu osiąga się przewidywalne i deterministyczne wykonanie instrukcji.
Przepływ sekwencyjny: Programowanie synchroniczne obsługuje sekwencyjny przebieg wykonywania, co oznacza, że każda instrukcja jest wykonywana sekwencyjnie, jedna po drugiej. Program językowy czeka na zakończenie instrukcji przed przejściem do następnej.
Prostota: Często programowanie synchroniczne uważa się za łatwe do zrozumienia, ponieważ możemy przewidzieć kolejność jego wykonywania. Generalnie jest to liniowe i łatwe do przewidzenia. Małe aplikacje dobrze nadają się do tworzenia w tych językach, ponieważ są w stanie obsłużyć krytyczną kolejność operacji.
Bezpośrednia obsługa błędów: W synchronicznym języku programowania obsługa błędów jest bardzo łatwa. Jeśli podczas wykonywania instrukcji wystąpi błąd, zgłosi błąd i program będzie mógł go przechwycić.
Krótko mówiąc, programowanie synchroniczne ma dwie podstawowe cechy, tj. w danym momencie wykonywane jest jedno zadanie, a następny zestaw kolejnych zadań zostanie rozpatrzony dopiero po zakończeniu bieżącego zadania. W ten sposób następuje sekwencyjne wykonanie kodu.
Takie zachowanie programowania podczas wykonywania instrukcji tworzy sytuację kodu blokowego, ponieważ każde zadanie musi czekać na zakończenie poprzedniego.
Kiedy jednak ludzie mówią o JavaScript, zawsze pojawia się zagadkowa odpowiedź, czy jest on synchroniczny, czy asynchroniczny.
W omówionych powyżej przykładach, gdy używaliśmy funkcji jako wywołania zwrotnego w funkcji filtrującej, była ona wykonywana synchronicznie. Dlatego nazywa się to wykonaniem synchronicznym. Funkcja filtrująca musi poczekać, aż funkcja większa zakończy wykonywanie.
Dlatego funkcja wywołania zwrotnego nazywana jest również blokowaniem wywołań zwrotnych, ponieważ blokuje wykonanie funkcji nadrzędnej, w której została wywołana.
Przede wszystkim JavaScript jest uważany za jednowątkowy, synchroniczny i blokujący z natury. Ale stosując kilka podejść, możemy sprawić, że będzie działać asynchronicznie w oparciu o różne scenariusze.
Teraz przyjrzyjmy się asynchronicznemu JavaScriptowi.
Asynchroniczny JavaScript
Asynchroniczny język programowania skupia się na zwiększeniu wydajności aplikacji. W takich scenariuszach można używać wywołań zwrotnych. Możemy przeanalizować asynchroniczne zachowanie JavaScriptu na poniższym przykładzie:
function greet(){ console.log('greet after 1 second') } setTimeout(greet, 1000)
Z powyższego przykładu funkcja setTimeout przyjmuje jako argumenty wywołanie zwrotne i czas w milisekundach. Wywołanie zwrotne zostanie wywołane po podanym czasie (tutaj 1 s). Krótko mówiąc, funkcja będzie czekać 1s na wykonanie. Teraz spójrz na poniższy kod:
function greet(){ console.log('greet after 1 second') } setTimeout(greet, 1000) console.log('first') console.log('Second')
Wyjście:
first Second greet after 1 second
Z powyższego kodu wynika, że komunikaty dziennika po setTimeout zostaną wykonane jako pierwsze po upłynięciu czasu. W związku z tym powstaje jedna sekunda, a następnie wiadomość powitalna po 1 sekundzie.
W JavaScript setTimeout jest funkcją asynchroniczną. Ilekroć wywołujemy funkcję setTimeout, rejestruje ona funkcję wywołania zwrotnego (w tym przypadku greet), która ma zostać wykonana po określonym opóźnieniu. Nie blokuje to jednak wykonania kolejnego kodu.
W powyższym przykładzie komunikaty dziennika są instrukcjami synchronicznymi, które są wykonywane natychmiast. Nie są one zależne od funkcji setTimeout. Dlatego wykonują i rejestrują swoje odpowiednie komunikaty na konsoli, nie czekając na opóźnienie określone w setTimeout.
Tymczasem pętla zdarzeń w JavaScript obsługuje zadania asynchroniczne. W tym wypadku czeka aż upłynie zadany czas (1 sekunda) i po upływie tego czasu pobiera funkcję wywołania zwrotnego (greet) i ją wykonuje.
Zatem drugi kod po funkcji setTimeout był wykonywany podczas działania w tle. To zachowanie umożliwia JavaScriptowi wykonywanie innych zadań podczas oczekiwania na zakończenie operacji asynchronicznej.
Musimy zrozumieć stos wywołań i kolejkę wywołań zwrotnych, aby obsłużyć zdarzenia asynchroniczne w JavaScript.
Rozważ poniższy obraz:
Podstawowe informacje o kompilacji Ubuntu
Z powyższego obrazu wynika, że typowy silnik JavaScript składa się z pamięci sterty i stosu wywołań. Stos wywołań wykonuje cały kod bez czekania po umieszczeniu go na stosie.
Pamięć sterty jest odpowiedzialna za alokację pamięci dla obiektów i funkcji w czasie wykonywania, gdy są one potrzebne.
Obecnie nasze silniki przeglądarek składają się z kilku internetowych interfejsów API, takich jak DOM, setTimeout, konsola, fetch itp., a silnik może uzyskać dostęp do tych interfejsów API za pomocą globalnego obiektu window. W następnym kroku niektóre pętle zdarzeń pełnią rolę strażnika, który wybiera żądania funkcji z kolejki wywołań zwrotnych i umieszcza je na stosie. Funkcje te, takie jak setTimeout, wymagają określonego czasu oczekiwania.
Wróćmy teraz do naszego przykładu, funkcji setTimeout; po napotkaniu funkcji licznik czasu zostaje zarejestrowany w kolejce wywołań zwrotnych. Następnie reszta kodu jest umieszczana na stosie wywołań i wykonywana, gdy funkcja osiągnie swój limit czasu, wygaśnie, a kolejka wywołania zwrotnego wypycha funkcję wywołania zwrotnego, która ma określoną logikę i jest zarejestrowana w funkcji limitu czasu . Tym samym zostanie on wykonany po określonym czasie.
Scenariusze piekła zwrotnego
Teraz omówiliśmy wywołania zwrotne, synchroniczne, asynchroniczne i inne istotne tematy dotyczące piekła wywołań zwrotnych. Rozumiemy, czym jest piekło wywołań zwrotnych w JavaScript.
Sytuacja, w której zagnieżdżonych jest wiele wywołań zwrotnych, nazywana jest piekłem wywołań zwrotnych, ponieważ kształt jej kodu przypomina piramidę, zwaną także „piramidą zagłady”.
Piekło wywołania zwrotnego utrudnia zrozumienie i utrzymanie kodu. Z taką sytuacją najczęściej spotykamy się pracując w węźle JS. Rozważmy na przykład poniższy przykład:
getArticlesData(20, (articles) => { console.log('article lists', articles); getUserData(article.username, (name) => { console.log(name); getAddress(name, (item) => { console.log(item); //This goes on and on... } })
W powyższym przykładzie getUserData przyjmuje nazwę użytkownika zależną od listy artykułów lub należy ją wyodrębnić z odpowiedzi getArticles znajdującej się w artykule. getAddress ma również podobną zależność, która zależy od odpowiedzi getUserData. Ta sytuacja nazywa się piekłem wywołań zwrotnych.
Java łącząca ciągi znaków
Wewnętrzne działanie piekła wywołania zwrotnego można zrozumieć na poniższym przykładzie:
Załóżmy, że musimy wykonać zadanie A. Do wykonania zadania potrzebujemy danych z zadania B. Podobnie; mamy różne zadania, które są od siebie zależne i wykonują się asynchronicznie. W ten sposób tworzy szereg funkcji wywołania zwrotnego.
Przyjrzyjmy się obietnicom w JavaScript i sposobowi, w jaki tworzą operacje asynchroniczne, co pozwala nam uniknąć pisania zagnieżdżonych wywołań zwrotnych.
JavaScript obiecuje
W JavaScript obietnice zostały wprowadzone w ES6. Jest to obiekt z powłoką syntaktyczną. Ze względu na asynchroniczne zachowanie jest to alternatywny sposób uniknięcia pisania wywołań zwrotnych dla operacji asynchronicznych. Obecnie interfejsy API sieci Web, takie jak fetch(), są implementowane przy użyciu obiecującego sposobu, który zapewnia skuteczny sposób dostępu do danych z serwera. Poprawiło to także czytelność kodu i pozwala uniknąć pisania zagnieżdżonych wywołań zwrotnych.
Obietnice w prawdziwym życiu wyrażają zaufanie między dwiema lub więcej osobami i zapewnienie, że dana rzecz na pewno się wydarzy. W JavaScript obietnica jest obiektem zapewniającym wygenerowanie pojedynczej wartości w przyszłości (jeśli jest to wymagane). Obietnica w JavaScript służy do zarządzania i rozwiązywania operacji asynchronicznych.
Obietnica zwraca obiekt, który zapewnia i reprezentuje zakończenie lub niepowodzenie operacji asynchronicznych oraz ich wynik. Jest to proxy dla wartości bez znajomości dokładnego wyniku. W przypadku akcji asynchronicznych przydatne jest podanie ostatecznej wartości sukcesu lub przyczyny niepowodzenia. Zatem metody asynchroniczne zwracają wartości podobnie jak metoda synchroniczna.
Ogólnie rzecz biorąc, obietnice mają następujące trzy stany:
- Spełniony: Stan spełniony ma miejsce, gdy zastosowana akcja została rozpatrzona lub pomyślnie zakończona.
- Oczekujące: stan Oczekujący ma miejsce, gdy żądanie jest w trakcie przetwarzania, a zastosowana akcja nie została rozwiązana ani odrzucona i nadal znajduje się w stanie początkowym.
- Odrzucone: Stan odrzucenia ma miejsce, gdy zastosowana akcja została odrzucona, co spowodowało niepowodzenie żądanej operacji. Przyczyną odrzucenia może być wszystko, łącznie z awarią serwera.
Składnia obietnic:
let newPromise = new Promise(function(resolve, reject) { // asynchronous call is made //Resolve or reject the data });
Poniżej znajduje się przykład zapisu obietnic:
To jest przykład pisania obietnicy.
function getArticleData(id) { return new Promise((resolve, reject) => { setTimeout(() => { console.log('Fetching data....'); resolve({ id: id, name: 'derik' }); }, 5000); }); } getArticleData('10').then(res=> console.log(res))
W powyższym przykładzie możemy zobaczyć, jak możemy efektywnie wykorzystać obietnice do wysłania żądania z serwera. Możemy zaobserwować, że czytelność kodu jest większa w powyższym kodzie niż w wywołaniach zwrotnych. Obietnice udostępniają metody takie jak .then() i .catch(), które pozwalają nam obsłużyć status operacji w przypadku powodzenia lub niepowodzenia. Możemy określić przypadki dla różnych stanów obietnic.
Async/Oczekiwanie w JavaScript
Jest to kolejny sposób na uniknięcie stosowania zagnieżdżonych wywołań zwrotnych. Async/Await pozwala nam znacznie efektywniej wykorzystywać obietnice. Możemy uniknąć łączenia metod .then() lub .catch(). Metody te są również zależne od funkcji wywołania zwrotnego.
Async/Await można precyzyjnie używać z Promise w celu poprawy wydajności aplikacji. Wewnętrznie rozwiązał obietnice i zapewnił wynik. Ponadto jest bardziej czytelna niż metody () lub catch().
Nie możemy używać Async/Await z normalnymi funkcjami wywołania zwrotnego. Aby z niego skorzystać, musimy uczynić funkcję asynchroniczną, pisząc słowo kluczowe async przed słowem kluczowym Function. Jednak wewnętrznie używa również łączenia łańcuchowego.
Poniżej znajduje się przykład Async/Await:
async function displayData() { try { const articleData = await getArticle(10); const placeData = await getPlaces(article.name); const cityData = await getCity(place) console.log(city); } catch (err) { console.log('Error: ', err.message); } } displayData();
Aby użyć funkcji Async/Await, należy określić funkcję za pomocą słowa kluczowego async, a wewnątrz funkcji należy zapisać słowo kluczowe Wait. Proces asynchroniczny zatrzyma wykonywanie do czasu rozwiązania lub odrzucenia obietnicy. Zostanie wznowione po rozdaniu Obietnicy. Po rozwiązaniu wartość wyrażenia oczekującego zostanie zapisana w zmiennej, która je przechowuje.
Streszczenie:
W skrócie, możemy uniknąć zagnieżdżonych wywołań zwrotnych, używając obietnic i async/await. Oprócz tego pomocne mogą być również inne podejścia, takie jak pisanie komentarzy i dzielenie kodu na osobne komponenty. Jednak obecnie programiści wolą używać async/await.
Wniosek:
Piekło wywołania zwrotnego w JavaScript określa się jako sytuację, w której wykonywana jest nadmierna liczba zagnieżdżonych funkcji wywołania zwrotnego. Zmniejsza czytelność kodu i konserwację. Sytuacja wywołania zwrotnego zwykle ma miejsce w przypadku asynchronicznych operacji żądań, takich jak wysyłanie wielu żądań do interfejsu API lub obsługa zdarzeń ze złożonymi zależnościami.
Aby lepiej zrozumieć piekło wywołań zwrotnych w JavaScript.
JavaScript traktuje wszystko jako obiekt, na przykład ciągi znaków, tablice i funkcje. Dlatego koncepcja wywołania zwrotnego pozwala nam przekazać funkcję jako argument innej funkcji. Funkcja wywołania zwrotnego zakończy wykonanie jako pierwsza, a funkcja nadrzędna zostanie wykonana później.
Funkcje wywołania zwrotnego są wykonywane asynchronicznie i umożliwiają dalsze działanie kodu bez czekania na zakończenie zadania asynchronicznego.