Hur fungerar Event Loop i JavaScript?

By rik

Att utveckla fullskalig produktionskod kan kräva en djup förståelse av språk som C++ och C. JavaScript, däremot, kan ofta användas med en grundläggande kunskap om språkets funktioner.

Koncept som callbacks till funktioner eller asynkron kod kan vara förvånansvärt enkla att implementera. Detta leder till att många JavaScript-utvecklare inte lägger så stor vikt vid de underliggande processerna. De engagerar sig inte fullt ut i att förstå de komplexa mekanismer som språket har abstraherat.

Som JavaScript-utvecklare blir det allt viktigare att förstå vad som verkligen händer bakom kulisserna och hur dessa abstraherade komplexiteter faktiskt fungerar. Denna kunskap hjälper oss att fatta mer informerade beslut, vilket i sin tur kan förbättra kodens prestanda betydligt.

Den här artikeln kommer att fokusera på ett centralt, men ofta missförstått begrepp i JavaScript: Event Loop.

Asynkron kod är oundviklig i JavaScript, men vad innebär det egentligen att kod körs asynkront? Det är här Event Loop kommer in i bilden.

Innan vi kan undersöka hur Event Loop fungerar, måste vi först definiera JavaScript och dess grundläggande funktioner.

Vad är JavaScript?

Innan vi går vidare, låt oss backa till grunderna. Vad är egentligen JavaScript? En definition skulle kunna vara:

JavaScript är ett tolkat, single-threaded, icke-blockerande, asynkront och samtidigt programmeringsspråk på hög nivå.

En bokstavlig definition? 🤔

Låt oss bryta ner den här definitionen.

Nyckelorden i sammanhanget av denna artikel är single-threaded, icke-blockerande, samtidigt och asynkront.

Single-threaded

En exekveringstråd är den minsta enhet av programmerad instruktion som kan hanteras oberoende av en schemaläggare. Ett single-threaded programmeringsspråk innebär att det bara kan utföra en uppgift eller operation åt gången. Det innebär att en hel process körs från början till slut utan avbrott eller stopp.

Detta skiljer sig från multi-threaded språk, där flera processer kan köras samtidigt på olika trådar utan att blockera varandra.

Men hur kan JavaScript vara single-threaded och icke-blockerande samtidigt?

Och vad innebär ”blockerande”?

Icke-blockerande

Det finns ingen strikt definition av blockering. Det hänvisar helt enkelt till processer som tar lång tid att slutföra inom tråden. Icke-blockerande innebär därmed att processer inte är långsamma.

Men, vänta. Vi sa att JavaScript körs på en enda tråd? Och vi sa också att det är icke-blockerande, vilket antyder att uppgifter körs snabbt i callstacken? Men hur? Vad händer med timers eller loopar?

Lugn. Vi kommer till det snart 😉.

Samtidigt

Samtidighet betyder att kod kan exekveras av mer än en tråd samtidigt.

Okej, nu blir det lite underligt. Hur kan JavaScript vara single-threaded men samtidigt, alltså utföra kod med mer än en tråd?

Asynkront

Asynkron programmering innebär att koden körs via en händelseloop. När en blockering sker, triggas händelsen. Blockeringkoden körs separat utan att stoppa huvudtråden. När blockeringkoden är klar, köas resultatet och skickas tillbaka till callstacken.

Men JavaScript har ju bara en enda tråd? Vad utför blockeringkoden medan annan kod i tråden körs?

Låt oss summera det vi har gått igenom hittills:

  • JavaScript är single-threaded.
  • JavaScript är icke-blockerande, dvs. långsamma processer stoppar inte exekveringen.
  • JavaScript är samtidigt, dvs. det kan köra kod i mer än en tråd samtidigt.
  • JavaScript är asynkront, dvs. det kör blockeringkod på annat ställe.

Men det som vi har nämnt är inte riktigt logiskt. Hur kan ett single-threaded språk vara icke-blockerande, samtidigt och asynkront samtidigt?

Låt oss dyka djupare ner i JavaScripts runtime-motorer, V8 kanske har dolda trådar som vi inte känner till.

V8-motorn

V8-motorn är en högpresterande runtime-motor med öppen källkod som används för webbaserad JavaScript-kompilering. Den är skriven i C++ av Google. De flesta webbläsare använder V8-motorn för att köra JavaScript och även den populära runtime-miljön Node.js.

Enkelt uttryckt är V8 ett C++-program som tar emot JavaScript-kod, kompilerar och exekverar den.

V8 utför två viktiga funktioner:

  • Minneshantering
  • Hantering av Call Stack

Tyvärr, vår misstanke var felaktig. V8 har endast en call stack, tänk på call stacken som själva tråden.

En tråd === en call stack === en exekvering i taget.

Bild – Hacker Noon

Men eftersom V8 endast har en call stack, hur kan JavaScript köra kod samtidigt och asynkront utan att blockera huvudtråden?

Låt oss ta reda på det genom att skriva ett enkelt men vanligt exempel med asynkron kod och analysera den.

JavaScript kör varje kodrad en efter en, i ordning (single-threaded). Första raden skrivs ut i konsolen, som förväntat. Men varför skrivs den sista raden ut före timeoutkoden? Varför väntar inte exekveringsprocessen på timeoutkoden (blockering) innan den går vidare till den sista raden?

Det verkar som om en annan tråd har hjälpt oss att utföra timeouten, eftersom vi vet att en tråd bara kan utföra en enda uppgift åt gången.

Låt oss snabbt ta en titt på V8:s källkod.

Men vad? Det finns inga timerfunktioner i V8, ingen DOM, inga events eller AJAX!…

Ja, precis! Events, DOM, timers med mera är inte en del av JavaScripts kärnimplementering. JavaScript följer strikt ECMAScript-specifikationerna, som ofta refereras till med dess versionsnummer (ES X).

Exekveringsflöde

Events, timers, AJAX-förfrågningar och dylikt tillhandahålls av webbläsarna och kallas ofta webb-API:er. Det är de som gör att en single-threaded JavaScript-kod kan vara icke-blockerande, samtidig och asynkron. Men hur då?

Det finns tre huvuddelar i exekveringsflödet för alla JavaScript-program: Call Stack, Web API och Task Queue.

Call Stack

En stack är en datastruktur där det senast tillagda elementet alltid är det första som tas bort från stacken. Tänk dig en stapel med tallrikar: endast den tallrik som lades på sist kan tas bort först. En Call Stack är inget annat än just det: en stack där uppgifter eller kod exekveras enligt denna princip.

Titta på exemplet nedan:

När du anropar funktionen printSquare() läggs den till i Call Stack. Funktionen printSquare() anropar square()-funktionen, som läggs till i stacken. Den i sin tur anropar multiply()-funktionen, som också hamnar i stacken. Eftersom multiply() returnerar ett värde och är det sista elementet som lades till i stacken, hanteras den först och tas bort. Sedan tas square() bort, och slutligen printSquare().

Webb-API

Det är här som kod som inte hanteras av V8-motorn körs för att undvika att ”blockera” huvudtråden. När Call Stack stöter på en webb-API-funktion, skickas processen omedelbart till Web API:et där den körs, vilket frigör Call Stack så att den kan utföra andra operationer.

Låt oss återgå till vårt setTimeout-exempel ovan:

När vi kör koden skickas första console.log till stacken och vi får utskriften nästan omedelbart. När vi når setTimeout, som hanteras av webbläsaren och inte är en del av V8, skickas den till Web API istället, vilket frigör stacken så att den kan utföra andra saker.

Medan timeouten fortfarande pågår, går stacken vidare till nästa rad och kör sista console.log, vilket förklarar varför vi får ut det före timeoutens utskrift. När timern är klar, händer något: console.log dyker magiskt upp i Call Stack igen!

Men hur då?

Event Loop

Innan vi förklarar händelseloopen, låt oss först titta på uppgiftskön.

Återigen, till vårt setTimeout-exempel. När Web API:et är klar med en uppgift, skickas den inte direkt tillbaka till Call Stack. Den skickas istället till Task Queue.

En kö är en datastruktur som fungerar enligt principen ”först in, först ut”. Uppgifter som har utförts av Web API:er och skickas till uppgiftskön går sedan tillbaka till Call Stack för att få resultatet utskrivet.

Men vänta. VAD ÄR DÅ EVENT LOOP?

Event Loop är en process som väntar på att Call Stack ska bli tom innan callbacks skickas från Task Queue till Call Stack. När stacken är tom, triggas Event Loop och kontrollerar Task Queue efter tillgängliga callbacks. Om det finns några, skickar den dem till Call Stack, väntar på att stacken blir tom igen och upprepar processen.

Diagrammet ovan visar det grundläggande flödet mellan Event Loop och Task Queue.

Slutsats

Även om detta är en grundläggande introduktion till asynkron programmering i JavaScript, ger den tillräcklig insikt för att förstå vad som händer under huven och hur JavaScript kan köras samtidigt och asynkront med endast en enda tråd.

JavaScript är alltid efterfrågat. Om du är nyfiken på att lära dig mer, rekommenderar jag att du kollar in den här Udemy-kursen.