WORDS
Concurrency-modell och eventloop i Javascript
Javascript har en modell för concurrency, samtidig exekvering, som bygger på en "eventloop". Den här modellen skiljer sig rätt mycket från modellerna som finns i andra programmeringsspråk, såsom C, C# och Java.
Koncept kopplade till exekvering
Följande avsnitt förklarar en teoretisk modell. Moderna Javascriptmotorer implementerar och optimerar kraftigt den beskrivna semantiken.
Visuell representation
Stack
Funktionsanrop skapar en stack med frames.
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7)); // returnerar 42
Vid anrop till bar
, kommer en första frame skapas, som innehåller bar
s argument och lokala variabler. När bar
anropar foo
, skapas en andra frame med foo
s argument och lokala variabler, som läggs ovanpå den första framen. När foo
returnerar, tas det översta frame-elementet bort från stacken (och lämnar bara bar
-anropets frame). När bar
returnerar är stacken tom.
Heap
Objekt allokeras i en heap, vilket bara är ett namn på ett stort, mestadels ostrukturerat område minne.
Kö
En exekveringsmiljö för Javascript (runtime) använder en meddelandekö, vilket är en lista med meddelanden som ska behandlas. Varje meddelande har en associerad funktion, som anropas för att hantera meddelandet.
Vid något tillfälle under eventloopen, börjar exekveringsmiljön hantera meddelandena i kön, och börjar med det äldsta. För att göra det, tas meddelandet bort från kön och dess tillhörande funktion anropas med meddelandet som en inputparameter. Som vanligt skapar ett funktionsanrop en ny stackframe för den funktionens bruk.
Hantering av funktioner fortsätter tills stacken är tom igen; sedan behandlar eventloopen nästa meddelande i kön (om det finns ett).
Eventloop
Eventloopen fick sitt namn genom sättet som den ofta implementeras, vilket vanligtvis påminner om:
while (queue.waitForMessage()) {
// Behandla nästa meddelande
queue.processNextMessage();
}
queue.waitForMessage()
väntar synkront på att ett meddelande ska anlända, om det inte finns något för tillfället.
Kör från A till Ö
Varje meddelande behandlas fullständigt innan något annat meddelande behandlas. Detta för med sig några trevliga egenskaper när man resonerar om sitt program, såsom det faktum att när än en funktion exekveras, så kan den inte avbrytas, utan körs klart innan någon annan kod kör (som kan ändra data som funktionen använder). Detta skiljer sig från exempelvis C, där en funktion som exekveras i en tråd, när som helst kan stoppas av exekveringssystemet för att istället köra annan kod i en annan tråd.
En nackdel med denna modell är att om ett meddelande tar för lång tid att slutföra, kan webbapplikationen inte behandla klick, skrollning eller annan användarinteraktion. Webbläsaren mitigerar detta genom dialogrutan "ett skript tar för lång tid att köra". En praxis att följa är att göra behandling av meddelanden kort, och om möjligt dela upp ett meddelande i flera meddelanden.
Lägga till meddelanden
I webbläsare läggs meddelanden till när ett event inträffar och det finns en eventlyssnare bunden till det. Om det inte finns någon lyssnare, går eventet förlorat. Så ett klick på ett element med en klickevent-hanterare lägger till ett meddelande - detsamma gäller alla andra event.
Funktionen setTimeout
anropas med 2 argument: ett meddelande att lägga till i kön och ett tidsvärde (frivilligt; standardvärdet är 0). Tidsvärdet representerar (den minsta) fördröjningen innan meddelandet faktiskt läggs till i kön. Om det inte finns något annat meddelande i kön, behandlas meddelandet direkt efter fördröjningen; om det däremot finns meddelanden, kommer setTimeout
-meddelandet behöva vänta på att andra meddelanden behandlas. Av den anledningen anger det andra argumentet en minimitid och inte en garanterad tid.
Här följer ett exempel som demonstrerar konceptet (setTimeout
körs inte omedelbart efter att dess timer löpt ut):
const s = new Date().getSeconds();
setTimeout(function() {
// skriver ut "2", vilket visar att återanropet (callbacken)
// inte anropas omedelbart efter 500 millisekunder
console.log("Körd efter " + (new Date().getSeconds() - s) + " sekunder");
}, 500);
while (true) {
if (new Date().getSeconds() - s >= 2) {
console.log("Fint, loopade i 2 sekunder");
break;
}
}
Nollfördröjning
Nollfördröjning betyder inte att återanropet (callbacken) kommer att köras efter noll millisekunder. Att anropa setTimeout
med fördröjning på 0 (noll) millisekunder exekverar inte återanropsfunktionen efter det givna intervallet.
Exekveringen beror på antalet väntande uppgifter i kön. I exemplet nedan skrivs "detta är bara ett meddelande" till konsolen innan meddelandet i återanropet behandlas, för fördröjningen är den minsta tid som måste gå innan exekveringsmiljön behandlar förfrågan, inte en garanterad tid.
setTimeout
måste helt enkelt vänta på att all kod för köade meddelanden fullbordas, oavsett om du angav en viss tidsgräns för din setTimeout
.
(function() {
console.log("detta är början");
setTimeout(function cb() {
console.log("detta är ett meddelande från återanrop");
});
console.log("detta är bara ett meddelande");
setTimeout(function cb1() {
console.log("detta är ett meddelande från återanrop1");
}, 0);
console.log("detta är slutet");
})();
// "detta är början"
// "detta är bara ett meddelande"
// "detta är slutet"
// observera att funktionen returnerar här (med odefinierat värde, undefined)
// "detta är ett meddelande från återanrop"
// "detta är ett meddelande från återanrop1"
Flera exekveringsmiljöer som kommunicerar med varandra
En web worker eller en cross-origin-iframe
har sin egen stack, heap och meddelandekö. Två skilda exekveringsmiljöer kan bara kommunicera genom att skicka meddelanden via postMessage
-metoden. Denna metod lägger till ett meddelande i den andra exekveringsmiljön om den senare lyssnar på message
-event.
Aldrig blockerad
En mycket intressant egenskap hos eventloopmodellen är att Javascript, till skillnad från många andra språk, aldrig blockerar. Hantering av I/O görs normalt via event och återanrop (callbacks), så när applikationen väntar på att en IndexedDB-fråga ska returnera eller att ett XHR-anrop ska returnera, kan den fortfarande arbeta med andra saker, såsom användarinput.
Det finns äldre undantag, exempelvis alert
och synkron XHR, men praxis är att undvika dem. Se upp, undantag från undantaget existerar (men är vanligtvis implementationsbuggar snarare än något annat).
Artikeln är en översättning av Concurrency model and Event Loop, ursprungligen skriven av Mozillas bidragsgivare under licensen CC-BY-SA 2.5.