logo

Callback Hell i JavaScript

JavaScript är ett asynkront (icke-blockerande) och entrådigt programmeringsspråk, vilket innebär att endast en process kan köras åt gången.

I programmeringsspråk hänvisar callback helvetet i allmänhet till ett ineffektivt sätt att skriva kod med asynkrona samtal. Den är också känd som Doompyramiden.

Återuppringningshelvetet i JavaScript hänvisas till som en situation där en överdriven mängd kapslade återuppringningsfunktioner exekveras. Det minskar kodläsbarhet och underhåll. Återuppringningshelvetet uppstår vanligtvis när man hanterar asynkrona förfrågningar, som att göra flera API-förfrågningar eller hantera händelser med komplexa beroenden.

För att bättre förstå callback helvetet i JavaScript, först förstå callbacks och händelseloopar i JavaScript.

Återuppringningar i JavaScript

JavaScript betraktar allt som ett objekt, till exempel strängar, arrayer och funktioner. Därför tillåter callback-konceptet oss att skicka funktionen som ett argument till en annan funktion. Återuppringningsfunktionen kommer att slutföra exekveringen först, och den överordnade funktionen kommer att exekveras senare.

Återuppringningsfunktionerna exekveras asynkront och tillåter koden att fortsätta köras utan att vänta på att slutföra den asynkrona uppgiften. När flera asynkrona uppgifter kombineras, och varje uppgift beror på dess tidigare uppgift, blir kodstrukturen komplicerad.

Låt oss förstå användningen och vikten av återuppringningar. Låt oss anta ett exempel vi har en funktion som tar tre parametrar en sträng och två tal. Vi vill ha lite utdata baserat på strängtexten med flera villkor.

Tänk på exemplet nedan:

 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)) 

Produktion:

 30 20 

Ovanstående kod kommer att fungera bra, men vi måste lägga till fler uppgifter för att göra koden skalbar. Antalet villkorliga uttalanden kommer också att fortsätta att öka, vilket kommer att leda till en rörig kodstruktur som måste optimeras och läsas.

Så vi kan skriva om koden på ett bättre sätt enligt följande:

 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)) 

Produktion:

 30 20 

Ändå kommer resultatet att vara detsamma. Men i exemplet ovan har vi definierat dess separata funktionskropp och skickat funktionen som en återuppringningsfunktion till funktionen expectResult. Därför, om vi vill utöka funktionaliteten för de förväntade resultaten så att vi kan skapa ett annat fungerande organ med en annan operation och använda den som återuppringningsfunktion, kommer det att göra det lättare att förstå och förbättra kodens läsbarhet.

Det finns andra olika exempel på återuppringningar tillgängliga i JavaScript-funktioner som stöds. Några vanliga exempel är händelseavlyssnare och arrayfunktioner som map, reduce, filter, etc.

För att bättre förstå det bör vi förstå JavaScripts pass-by-värde och pass-by-referens.

JavaScript stöder två typer av datatyper som är primitiva och icke-primitiva. Primitiva datatyper är odefinierade, null, strängar och booleska, som inte kan ändras, eller vi kan säga oföränderliga jämförelsevis; icke-primitiva datatyper är arrayer, funktioner och objekt som kan ändras eller ändras.

Pass by reference skickar referensadressen för en entitet, som en funktion kan tas som ett argument. Så, om värdet inom den funktionen ändras, kommer det att ändra det ursprungliga värdet, som är tillgängligt utanför funktionen.

Jämförelsevis ändrar inte begreppet pass-by-value sitt ursprungliga värde, vilket är tillgängligt utanför funktionskroppen. Istället kommer den att kopiera värdet till två olika platser genom att använda deras minne. JavaScript identifierade alla objekt genom deras referens.

I JavaScript lyssnar addEventListener efter händelser som klick, mouseover och mouseout och tar det andra argumentet som en funktion som kommer att exekveras när händelsen utlöses. Denna funktion används som referenskoncept och skickas utan parentes.

Betrakta exemplet nedan; i det här exemplet har vi skickat en hälsningsfunktion som ett argument till addEventListener som callback-funktionen. Den kommer att anropas när klickhändelsen utlöses:

Test.html:

 Javascript Callback Example <h3>Javascript Callback</h3> Click Here to Console const button = document.getElementById(&apos;btn&apos;); const greet=()=&gt;{ console.log(&apos;Hello, How are you?&apos;) } button.addEventListener(&apos;click&apos;, greet) 

Produktion:

Callback Hell i JavaScript

I exemplet ovan har vi skickat en hälsningsfunktion som ett argument till addEventListener som callback-funktionen. Den kommer att anropas när klickhändelsen utlöses.

På samma sätt är filtret också ett exempel på återuppringningsfunktionen. Om vi ​​använder ett filter för att iterera en array kommer det att ta ytterligare en callback-funktion som ett argument för att bearbeta arraydata. Betrakta exemplet nedan; i det här exemplet använder vi funktionen större för att skriva ut talet större än 5 i arrayen. Vi använder isGreater-funktionen som en callback-funktion i filtermetoden.

 const arr = [3,10,6,7] const isGreater = num =&gt; num &gt; 5 console.log(arr.filter(isGreater)) 

Produktion:

 [ 10, 6, 7 ] 

Ovanstående exempel visar att den större funktionen används som en återuppringningsfunktion i filtermetoden.

För att bättre förstå Callbacks, Event loopar i JavaScript, låt oss diskutera synkron och asynkron JavaScript:

Synkron JavaScript

Låt oss förstå vad som är funktionerna i ett synkront programmeringsspråk. Synkron programmering har följande funktioner:

Blockerande körning: Det synkrona programmeringsspråket stöder blockeringsexekveringstekniken vilket innebär att det blockerar exekveringen av efterföljande satser som de befintliga satserna kommer att exekveras. Därmed uppnås den förutsägbara och deterministiska exekveringen av satserna.

Sekventiellt flöde: Synkron programmering stöder det sekventiella flödet av exekvering, vilket innebär att varje sats exekveras på ett sekventiellt sätt som en efter en. Språkprogrammet väntar på att ett uttalande ska slutföras innan det går vidare till nästa.

Enkelhet: Ofta anses den synkrona programmeringen vara lätt att förstå eftersom vi kan förutsäga dess ordningsföljd för exekveringsflödet. I allmänhet är det linjärt och lätt att förutsäga. De små applikationerna är bra att utveckla på dessa språk eftersom de kan hantera den kritiska ordningen av operationer.

Direkt felhantering: I ett synkront programmeringsspråk är felhanteringen mycket enkel. Om ett fel inträffar när en sats körs, kommer den att skapa ett fel och programmet kan fånga det.

I ett nötskal har synkron programmering två kärnfunktioner, det vill säga att en enda uppgift utförs åt gången, och nästa uppsättning av följande uppgifter kommer bara att åtgärdas när den aktuella uppgiften är klar. Därigenom följer den en sekventiell kodexekvering.

Detta beteende hos programmeringen när en sats körs, skapar en situation med blockkod eftersom varje jobb måste vänta på att det föregående jobbet ska slutföras.

Men när folk pratar om JavaScript har det alltid varit ett förbryllande svar oavsett om det är synkront eller asynkront.

I de ovan diskuterade exemplen, när vi använde en funktion som en callback i filterfunktionen, kördes den synkront. Därför kallas det en synkron exekvering. Filterfunktionen måste vänta tills den större funktionen slutförs.

Därför kallas återuppringningsfunktionen också för att blockera återuppringningar, eftersom den blockerar exekveringen av den överordnade funktionen i vilken den anropades.

I första hand anses JavaScript vara entrådad synkron och blockerande till sin natur. Men med några få metoder kan vi få det att fungera asynkront baserat på olika scenarier.

Låt oss nu förstå asynkron JavaScript.

Asynkron JavaScript

Det asynkrona programmeringsspråket fokuserar på att förbättra applikationens prestanda. Återuppringningarna kan användas i sådana scenarier. Vi kan analysera JavaScripts asynkrona beteende genom exemplet nedan:

 function greet(){ console.log(&apos;greet after 1 second&apos;) } setTimeout(greet, 1000) 

Från exemplet ovan tar funktionen setTimeout in en återuppringning och tid i millisekunder som argument. Återuppringningen åberopas efter den angivna tiden (här 1s). I ett nötskal, kommer funktionen att vänta i 1s på att den körs. Titta nu på koden nedan:

java-program
 function greet(){ console.log(&apos;greet after 1 second&apos;) } setTimeout(greet, 1000) console.log(&apos;first&apos;) console.log(&apos;Second&apos;) 

Produktion:

 first Second greet after 1 second 

Från ovanstående kod kommer loggmeddelandena efter setTimeout att exekveras först medan timern passerar. Därför resulterar det i en sekund och sedan hälsningsmeddelandet efter 1 sekunds tidsintervall.

I JavaScript är setTimeout en asynkron funktion. När vi anropar setTimeout-funktionen, registrerar den en återuppringningsfunktion (greet i detta fall) som ska exekveras efter den angivna fördröjningen. Det blockerar dock inte exekveringen av den efterföljande koden.

I exemplet ovan är loggmeddelandena de synkrona satserna som körs omedelbart. De är inte beroende av setTimeout-funktionen. Därför exekverar och loggar de sina respektive meddelanden till konsolen utan att vänta på fördröjningen som anges i setTimeout.

Under tiden hanterar händelseslingan i JavaScript de asynkrona uppgifterna. I det här fallet väntar den på att den angivna fördröjningen (1 sekund) ska passera, och efter att den tiden har gått hämtar den återuppringningsfunktionen (greet) och kör den.

Således kördes den andra koden efter setTimeout-funktionen medan den kördes i bakgrunden. Detta beteende gör att JavaScript kan utföra andra uppgifter i väntan på att den asynkrona operationen ska slutföras.

Vi måste förstå samtalsstacken och callback-kön för att hantera de asynkrona händelserna i JavaScript.

Tänk på bilden nedan:

Callback Hell i JavaScript

Från bilden ovan består en typisk JavaScript-motor av ett heapminne och en samtalsstack. Anropsstacken exekverar all kod utan att vänta när den skjuts till stacken.

Högminnet ansvarar för att allokera minnet för objekt och funktioner vid körning närhelst de behövs.

Nu består våra webbläsarmotorer av flera webb-API:er såsom DOM, setTimeout, console, fetch, etc., och motorn kan komma åt dessa API:er med hjälp av det globala fönsterobjektet. I nästa steg spelar vissa händelseloopar rollen som gatekeeper som väljer funktionsbegäranden i återuppringningskön och skjuter in dem i stacken. Dessa funktioner, såsom setTimeout, kräver en viss väntetid.

Låt oss nu gå tillbaka till vårt exempel, setTimeout-funktionen; när funktionen påträffas registreras timern i återuppringningskön. Efter detta trycks resten av koden in i anropsstacken och exekveras när funktionen når sin timergräns, den har löpt ut och återuppringningskön trycker på återuppringningsfunktionen, som har den specificerade logiken och registreras i timeoutfunktionen . Den kommer alltså att exekveras efter den angivna tiden.

Callback Hell Scenarios

Nu har vi diskuterat återuppringningar, synkrona, asynkrona och andra relevanta ämnen för återuppringningshelvetet. Låt oss förstå vad callback helvetet är i JavaScript.

Situationen när flera återuppringningar är kapslade är känd som återuppringningshelvetet eftersom dess kodform ser ut som en pyramid, som också kallas 'undergångens pyramid'.

Återuppringningshelvetet gör det svårare att förstå och underhålla koden. Vi kan mest se den här situationen när vi arbetar i nod JS. Tänk till exempel på följande exempel:

 getArticlesData(20, (articles) =&gt; { console.log(&apos;article lists&apos;, articles); getUserData(article.username, (name) =&gt; { console.log(name); getAddress(name, (item) =&gt; { console.log(item); //This goes on and on... } }) 

I exemplet ovan tar getUserData ett användarnamn som är beroende av artikellistan eller som måste extraheras getArticles-svar som finns inuti artikeln. getAddress har också ett liknande beroende, vilket är beroende av getUserDatas svar. Denna situation kallas callback helvete.

Det interna arbetet i callback-helvetet kan förstås med exemplet nedan:

Låt oss förstå att vi behöver utföra uppgift A. För att utföra en uppgift behöver vi lite data från uppgiften B. På samma sätt; vi har olika uppgifter som är beroende av varandra och exekverar asynkront. Således skapar den en serie återuppringningsfunktioner.

Låt oss förstå löftena i JavaScript och hur de skapar asynkrona operationer, vilket gör att vi kan undvika att skriva kapslade återuppringningar.

JavaScript lovar

I JavaScript infördes löften i ES6. Det är ett föremål med en syntaktisk beläggning. På grund av dess asynkrona beteende är det ett alternativt sätt att undvika att skriva återuppringningar för asynkrona operationer. Nuförtiden implementeras webb-API:er som fetch() med hjälp av det lovande, vilket ger ett effektivt sätt att komma åt data från servern. Det förbättrade också kodens läsbarhet och är ett sätt att undvika att skriva kapslade återuppringningar.

Löften i det verkliga livet uttrycker förtroende mellan två eller flera personer och en försäkran om att en viss sak säkert kommer att hända. I JavaScript är ett löfte ett objekt som säkerställer att ett enda värde produceras i framtiden (vid behov). Promise i JavaScript används för att hantera och hantera asynkrona operationer.

Löftet returnerar ett objekt som säkerställer och representerar fullbordandet eller misslyckandet av asynkrona operationer och dess utdata. Det är en proxy för ett värde utan att veta den exakta utgången. Det är användbart för asynkrona åtgärder för att ge ett eventuellt framgångsvärde eller orsak till misslyckande. Således returnerar de asynkrona metoderna värdena som en synkron metod.

I allmänhet har löftena följande tre tillstånd:

  • Uppfyllt: Uppfyllt tillstånd är när en tillämpad åtgärd har lösts eller slutförts framgångsrikt.
  • Väntande: Väntande tillstånd är när begäran pågår och den tillämpade åtgärden varken har lösts eller avvisats och är fortfarande i sitt ursprungliga tillstånd.
  • Avvisad: Det avvisade tillståndet är när den tillämpade åtgärden har avvisats, vilket gör att den önskade åtgärden misslyckas. Orsaken till avvisningen kan vara vad som helst, inklusive att servern är nere.

Syntaxen för löftena:

 let newPromise = new Promise(function(resolve, reject) { // asynchronous call is made //Resolve or reject the data }); 

Nedan är ett exempel på hur du skriver löftena:

Detta är ett exempel på att skriva ett löfte.

 function getArticleData(id) { return new Promise((resolve, reject) =&gt; { setTimeout(() =&gt; { console.log(&apos;Fetching data....&apos;); resolve({ id: id, name: &apos;derik&apos; }); }, 5000); }); } getArticleData(&apos;10&apos;).then(res=&gt; console.log(res)) 

I exemplet ovan kan vi se hur vi effektivt kan använda löftena för att göra en förfrågan från servern. Vi kan observera att kodens läsbarhet är ökad i ovanstående kod än i callbacks. Löften tillhandahåller metoder som .then() och .catch(), som tillåter oss att hantera operationsstatus vid framgång eller misslyckande. Vi kan specificera fallen för löftenas olika tillstånd.

Async/Await i JavaScript

Det är ett annat sätt att undvika användningen av kapslade återuppringningar. Async/ Await låter oss använda löftena mycket mer effektivt. Vi kan undvika att använda .then() eller .catch() metodkedja. Dessa metoder är också beroende av återuppringningsfunktionerna.

Async/Await kan användas exakt med Promise för att förbättra applikationens prestanda. Det löste löftena internt och gav resultatet. Dessutom är den mer läsbar än () eller catch() metoderna.

Vi kan inte använda Async/Await med normala återuppringningsfunktioner. För att använda det måste vi göra en funktion asynkron genom att skriva ett asynkront nyckelord före funktionsnyckelordet. Men internt använder den också chaining.

Nedan är ett exempel på 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(&apos;Error: &apos;, err.message); } } displayData(); 

För att använda Async/Await måste funktionen anges med nyckelordet async, och nyckelordet await ska skrivas inuti funktionen. Asynkroniseringen kommer att stoppa dess exekvering tills löftet är löst eller avvisat. Det kommer att återupptas när löftet delas ut. När det har lösts kommer värdet på await-uttrycket att lagras i variabeln som innehåller det.

Sammanfattning:

I ett nötskal kan vi undvika kapslade återuppringningar genom att använda löftena och async/await. Förutom dessa kan vi följa andra tillvägagångssätt, som att skriva kommentarer, och att dela upp koden i separata komponenter kan också vara till hjälp. Men nuförtiden föredrar utvecklarna användningen av async/await.

Slutsats:

Återuppringningshelvetet i JavaScript hänvisas till som en situation där en överdriven mängd kapslade återuppringningsfunktioner exekveras. Det minskar kodläsbarhet och underhåll. Återuppringningshelvetet uppstår vanligtvis när man hanterar asynkrona förfrågningar, som att göra flera API-förfrågningar eller hantera händelser med komplexa beroenden.

För att bättre förstå callback helvetet i JavaScript.

JavaScript betraktar allt som ett objekt, till exempel strängar, arrayer och funktioner. Därför tillåter callback-konceptet oss att skicka funktionen som ett argument till en annan funktion. Återuppringningsfunktionen kommer att slutföra exekveringen först, och den överordnade funktionen kommer att exekveras senare.

Återuppringningsfunktionerna exekveras asynkront och tillåter koden att fortsätta köras utan att vänta på att slutföra den asynkrona uppgiften.