Løs Mysteriet: Hvordan JavaScript Kjører Asynkront?

Å mestre fullskala produksjonskode kan kreve dyp innsikt i språk som C++ og C, men JavaScript lar deg ofte komme i gang med en grunnleggende forståelse av språket.

Begreper som å sende tilbakekall til funksjoner eller å skrive asynkron kode er som regel ikke så komplisert å implementere. Dette fører til at mange JavaScript-utviklere ikke fokuserer like mye på det som skjer i bakgrunnen. De legger ikke vekt på å forstå kompleksiteten som språket har abstrahert fra dem.

Som JavaScript-utvikler er det stadig viktigere å forstå de underliggende mekanismene og hvordan disse kompleksitetene fungerer. Dette gir oss bedre forutsetninger for å ta informerte valg, noe som igjen kan forbedre kodeytelsen vår betydelig.

Denne artikkelen tar for seg et av de viktigste, men ofte misforståtte konseptene i JavaScript: hendelsesløkken (event loop)!

Å skrive asynkron kode er essensielt i JavaScript, men hva betyr det egentlig at kode kjører asynkront? Det er her hendelsesløkken kommer inn i bildet.

Før vi kan dykke ned i hvordan hendelsesløkken fungerer, må vi først forstå hva JavaScript er og hvordan det opererer!

Hva er JavaScript?

La oss ta et steg tilbake og se på det grunnleggende. Hva er egentlig JavaScript? Vi kan definere JavaScript som:

JavaScript er et høynivå, tolket, enkelttrådet, ikke-blokkerende, asynkront og samtidig språk.

En definisjon fra læreboka? 🤔

La oss bryte den ned!

For denne artikkelen er de viktigste begrepene enkelttrådet, ikke-blokkerende, samtidig og asynkront.

Enkeltrådet

En utførelsestråd er den minste sekvensen av programmerte instruksjoner som kan administreres uavhengig av en planlegger. Et programmeringsspråk er enkelttrådet hvis det kun kan utføre én oppgave eller operasjon om gangen. Dette innebærer at den vil fullføre en hel prosess fra start til slutt uten avbrudd.

Dette står i kontrast til flertrådede språk der flere prosesser kan kjøre på flere tråder samtidig uten å blokkere hverandre.

Hvordan kan JavaScript være enkelttrådet og ikke-blokkerende på samme tid?

Men hva vil det si å blokkere?

Ikke-blokkerende

Det finnes ingen formell definisjon av blokkering; det handler rett og slett om prosesser som tar lang tid å kjøre på tråden. Dermed betyr ikke-blokkerende prosesser som ikke er trege på tråden.

Men jeg sa jo at JavaScript kjører på en enkelt tråd? Og jeg sa også at det er ikke-blokkerende, noe som betyr at oppgaver kjører raskt på anropsstakken? Men hvordan? Hva med tidtakere? Løkker?

Slapp av! Vi skal finne ut av det snart 😉.

Samtidig

Samtidig betyr at kode kjøres parallelt av mer enn én tråd.

Ok, nå begynner det å bli interessant. Hvordan kan JavaScript være enkelttrådet og samtidig? Altså, kjøre kode med mer enn én tråd?

Asynkron

Asynkron programmering innebærer at kode kjøres i en hendelsessløyfe. Når en blokkerende operasjon oppstår, aktiveres hendelsen. Den blokkerende koden fortsetter å kjøre uten å blokkere hovedutførelsestråden. Når den blokkerende koden er ferdig, settes resultatet av operasjonene i en kø og skyves tilbake til stabelen.

Men JavaScript har jo bare en enkelt tråd? Hva kjører denne blokkerende koden mens andre deler av koden blir utført i tråden?

La oss oppsummere det vi har gått gjennom så langt.

  • JavaScript er enkelttrådet.
  • JavaScript er ikke-blokkerende, det vil si at langsomme prosesser ikke stopper utførelsen.
  • JavaScript er samtidig, som betyr at det utfører koden sin i mer enn én tråd samtidig.
  • JavaScript er asynkront, altså at blokkerende kode kjøres et annet sted.

Men dette gir ikke helt mening, hvordan kan et enkelttrådet språk være ikke-blokkerende, samtidig og asynkront?

La oss dykke dypere og se på JavaScript-runtime-motorene, som V8, kanskje de har noen skjulte tråder vi ikke vet om.

V8-motoren

V8-motoren er en høyytelses, åpen kildekode-runtime-motor for webmontering for JavaScript, skrevet i C++ av Google. De fleste nettlesere bruker V8-motoren for å kjøre JavaScript, og det populære node js runtime-miljøet bruker den også.

Enkelt sagt er V8 et C++-program som mottar JavaScript-kode, kompilerer og kjører den.

V8 har to hovedoppgaver:

  • Tildeling av haugminne
  • Kontekst for utførelse av anropsstabelen

Dessverre var mistanken vår feil. V8 har bare en anropsstabel, tenk på anropsstabelen som tråden.

En tråd === en anropsstabel === en utførelse om gangen.

Bilde – Hacker Noon

Siden V8 bare har en anropsstabel, hvordan kan JavaScript kjøre samtidig og asynkront uten å blokkere hovedutførelsestråden?

La oss prøve å finne ut av det ved å se på en enkel, men vanlig asynkron kode og analysere den sammen.

JavaScript kjører hver kodelinje etter hverandre, linje for linje (enkelttrådet). Den første linjen skrives som forventet ut i konsollen, men hvorfor skrives den siste linjen ut før tidsavbruddskoden? Hvorfor venter ikke utførelsesprosessen på tidsavbruddskoden (blokkering) før den fortsetter til den siste linjen?

Det ser ut som en annen tråd har hjulpet oss med å utføre tidsavbruddet, siden vi vet at en tråd kun kan utføre en oppgave om gangen.

La oss ta en titt i V8-kildekoden.

Vent litt! Hva? Det er ingen timerfunksjoner i V8, ingen DOM? Ingen hendelser? Ingen AJAX?… Jepp!!!

Hendelser, DOM, tidtakere og lignende er ikke en del av JavaScripts kjerneimplementering. JavaScript følger strengt EcmaScripts-spesifikasjonene, og forskjellige versjoner refereres ofte til i henhold til Ecma Scripts Specifications (ES X).

Arbeidsflyt for utførelse

Hendelser, tidtakere og Ajax-forespørsler leveres av nettleserne på klientsiden og blir ofte referert til som Web API-er. Det er disse som gjør at det enkelttrådede JavaScript kan være ikke-blokkerende, samtidig og asynkront! Men hvordan?

Det er tre hovedseksjoner i arbeidsflyten for utførelse av ethvert JavaScript-program: anropsstakken, web-API-et og oppgavekøen.

Anropsstakken

En stabel er en datastruktur der det siste elementet som legges til, alltid er det første som fjernes fra stabelen. Du kan tenke på det som en stabel med tallerkener der kun den øverste tallerkenen kan fjernes først. En anropsstakk er enkelt og greit en stabeldatastruktur der oppgaver eller kode utføres sekvensielt.

La oss se på eksemplet nedenfor;

Kilde – https://youtu.be/8aGhZQkoFbQ

Når du kaller funksjonen printSquare(), skyves den på anropsstakken. Funksjonen printSquare() kaller funksjonen square(). Funksjonen square() skyves inn på stabelen og kaller funksjonen multiply(). Funksjonen multiply() skyves inn på stabelen. Siden funksjonen multiply() returnerer en verdi og er den siste som ble lagt til, løses den først og fjernes fra stabelen, deretter fjernes square()-funksjonen og til slutt printSquare()-funksjonen.

Web API

Det er her kode som ikke håndteres av V8-motoren, kjøres for å unngå å «blokkere» hovedutførelsestråden. Når anropsstakken støter på en web-API-funksjon, overføres prosessen umiddelbart til web-API-et, hvor den utføres og frigjør anropsstakken slik at den kan utføre andre operasjoner.

La oss gå tilbake til eksemplet vårt med setTimeout;

Når vi kjører koden, skyves den første console.log-linjen til stabelen, og vi får utdataene nesten umiddelbart. Når vi kommer til timeout, håndteres timere av nettleseren og er ikke en del av V8s kjerneimplementering. Den skyves i stedet til Web API, og frigjør stabelen slik at den kan utføre andre operasjoner.

Mens tidsavbruddet fortsatt kjører, fortsetter stabelen til neste linje og kjører den siste console.log, som forklarer hvorfor vi får utskriften før timeout-utskriften. Når timeren er fullført, skjer det noe. console.log går inn, og timeren dukker på magisk vis opp igjen i anropsstakken!

Hvordan?

Hendelsessløyfen

Før vi diskuterer hendelsesløkken, la oss se på funksjonen til oppgavekøen.

Tilbake til eksemplet vårt med tidsavbrudd, når Web API er ferdig med å utføre oppgaven, skyver den den ikke bare tilbake til anropsstakken automatisk. Den går til oppgavekøen.

En kø er en datastruktur som fungerer etter «først inn, først ut»-prinsippet. Når oppgaver legges i køen, kommer de ut i samme rekkefølge. Oppgaver som er utført av web-API-ene, og som skyves til oppgavekøen, går deretter tilbake til anropsstakken for å få resultatet skrevet ut.

Men vent. HVA I ALLE DAGER ER HENDELSESSLØKKEN???

Kilde – https://youtu.be/8aGhZQkoFbQ

Hendelsesløkken er en prosess som venter på at anropsstakken er ledig før tilbakeringinger fra oppgavekøen skyves inn i anropsstakken. Når stakken er klar, starter hendelsesløkken og sjekker oppgavekøen for tilgjengelige tilbakeringinger. Hvis det finnes noen, skyver den dem til anropsstakken, venter på at anropsstakken er ledig igjen og gjentar samme prosess.

Kilde – https://www.quora.com/How-does-an-event-loop-work/answer/Timothy-Maxwell

Diagrammet ovenfor illustrerer den grunnleggende arbeidsflyten mellom hendelsessløyfen og oppgavekøen.

Konklusjon

Selv om dette bare er en grunnleggende introduksjon, gir konseptet med asynkron programmering i JavaScript nok innsikt til å forstå hva som skjer bak kulissene og hvordan JavaScript er i stand til å kjøre samtidig og asynkront med bare en enkelt tråd.

JavaScript er alltid i utvikling, og hvis du er nysgjerrig på å lære mer, anbefaler jeg at du sjekker ut dette Udemy-kurset.