JavaScript is een single-threaded taal en tegelijkertijd ook non-blocking, asynchroon en concurrent. Dit artikel legt je uit hoe dat in zijn werk gaat.
Runtime
JavaScript is een geïnterpreteerde taal, geen gecompileerde taal. Dit betekent dat het een interpreter nodig heeft die de JS code naar een machinecode. Er zijn verschillende soorten interpreters (engines genoemd). De populairste browser engines zijn V8 (Chrome), Quantum (Firefox) en WebKit (Safari). Overigens wordt V8 ook gebruikt in een populaire niet-browser runtime, Node.js.
Elke engine bevat een memory heap, een call stack, een event loop, een callback queue en een WebAPI met HTTP requests, timers, events, etc., allemaal op hun eigen manier geïmplementeerd voor een snellere en veiligere interpretatie van de JS code.
Basis JS runtime-architectuur. Auteur: Alex Zlatkov
Enkele draad
Een single-thread taal is een taal met een enkele aanroepstapel en een enkele geheugenhoop. Dit betekent dat er maar één ding tegelijk wordt uitgevoerd.
A stapel is een doorlopend geheugengebied, dat lokale context toewijst voor elke uitgevoerde functie.
A hoop is een veel groter gebied, waarin alles wordt opgeslagen dat dynamisch wordt toegewezen.
A oproepstapel is een gegevensstructuur die in principe bijhoudt waar we ons in het programma bevinden.
Stapel oproepen
Laten we een eenvoudige code schrijven en bijhouden wat er gebeurt op de aanroepstapel.
Zoals je kunt zien, worden de functies toegevoegd aan de stack, uitgevoerd en later verwijderd. Het is de zogenaamde LIFO manier - Last In, First Out. Elk item in de aanroepstapel wordt een stapelframe.
Kennis van de aanroepstapel is nuttig voor het lezen van foutstapelsporen. Over het algemeen staat de exacte reden voor de fout bovenaan in de eerste regel, hoewel de volgorde van code-uitvoering bottom-up is.
Soms kunt u te maken krijgen met een populaire fout, gemeld door Maximale grootte aanroepstapel overschreden. Het is eenvoudig om dit te krijgen met behulp van recursie:
functie foo() {
foo()
}
foo()
en onze browser of terminal loopt vast. Elke browser, zelfs de verschillende versies, heeft een andere limiet voor de grootte van de aanroepstapel. In de overgrote meerderheid van de gevallen zijn deze voldoende en moet het probleem ergens anders worden gezocht.
Geblokkeerde aanroepstapel
Hier is een voorbeeld van het blokkeren van de JS thread. Laten we proberen een foo bestand en een bar met behulp van de Knooppunt.js synchrone functie readFileSync.
Dit is een geluste GIF. Zoals je ziet, wacht de JS-engine tot de eerste oproep in readFileSync is voltooid. Maar dit zal niet gebeuren omdat er geen foo bestand, dus de tweede functie zal nooit worden aangeroepen.
Asynchroon gedrag
JS kan echter ook non-blocking zijn en zich gedragen alsof het multi-threaded is. Dit betekent dat het niet wacht op het antwoord van een API-aanroep, I/O-gebeurtenissen, enzovoort, en door kan gaan met het uitvoeren van code. Dit is mogelijk dankzij de JS engines die (onder de motorkap) echte multi-threading talen gebruiken, zoals C++ (Chrome) of Rust (Firefox). Ze voorzien ons van de Web API onder de browserkappen of bijv. I/O API onder Node.js.
In de bovenstaande GIF kunnen we zien dat de eerste functie naar de aanroepstapel wordt geduwd en Hoi wordt onmiddellijk uitgevoerd in de console.
Vervolgens roepen we de setTimeout functie van de WebAPI van de browser. Het gaat naar de oproepstapel en zijn asynchrone callback foo functie gaat naar de wachtrij van de WebApi, waar het wacht op de oproep, die is ingesteld om na 3 seconden te gebeuren.
Ondertussen gaat het programma verder met de code en zien we Hallo. Ik ben niet geblokkeerd in de console.
Nadat deze is aangeroepen, gaat elke functie in de WebAPI-wachtrij naar de Terugbel wachtrij. Het is waar functies wachten tot de aanroepstapel leeg is. Als dat gebeurt, worden ze er één voor één naartoe verplaatst.
Dus toen onze setTimeout timer klaar is met aftellen, zal onze foo functie gaat naar de callbackwachtrij, wacht tot de oproepstapel beschikbaar komt, gaat daarheen, wordt uitgevoerd en we zien Hi van asynchrone callback in de console.
Gebeurtenis lus
De vraag is, hoe weet de runtime dat de aanroepstapel leeg is en hoe wordt de gebeurtenis in de callbackwachtrij aangeroepen? Maak kennis met de event-lus. Dit is een onderdeel van de JS-engine. Dit proces controleert voortdurend of de aanroepstapel leeg is en als dat zo is, controleert het of er een gebeurtenis in de callbackwachtrij staat die wacht om te worden aangeroepen.
Dat is alle magie achter de schermen!
De theorie samenvatten
Concurrency en parallellisme
Concurrentie betekent meerdere taken tegelijkertijd uitvoeren, maar niet tegelijkertijd. Twee taken werken bijvoorbeeld in overlappende tijdsperioden.
Parallellisme betekent twee of meer taken tegelijkertijd uitvoeren, bijvoorbeeld meerdere berekeningen tegelijkertijd uitvoeren.
Draden en processen
Draden zijn een opeenvolging van code die onafhankelijk van elkaar kunnen worden uitgevoerd.
Proces is een instantie van een lopend programma. Een programma kan meerdere processen hebben.
Synchroon en asynchroon
In synchroon Bij programmeren worden taken na elkaar uitgevoerd. Elke taak wacht tot een vorige taak is voltooid en wordt dan pas uitgevoerd.
In asynchroon programmering, wanneer een taak is uitgevoerd, kun je overschakelen naar een andere taak zonder te wachten tot de vorige is voltooid.
Synchroon en asynchroon in een omgeving met één of meerdere threads
Synchroon met één thread: Taken worden na elkaar uitgevoerd. Elke taak wacht tot de vorige taak is uitgevoerd.
Synchroon met meerdere threads: Taken worden uitgevoerd in verschillende threads, maar wachten op andere uitvoerende taken op een andere thread.
Asynchroon met één thread: Taken worden uitgevoerd zonder te wachten tot een andere taak klaar is. Op een gegeven moment kan slechts één taak worden uitgevoerd.
Asynchroon met meerdere threads: Taken worden in verschillende threads uitgevoerd zonder te wachten tot andere taken klaar zijn en voltooien hun uitvoering onafhankelijk van elkaar.
JavaScript classificatie
Als we kijken naar hoe JS engines onder de motorkap werken, kunnen we JS classificeren als een asynchrone en single-threaded geïnterpreteerde taal. Het woord "geïnterpreteerd" is erg belangrijk omdat het betekent dat de taal altijd runtime-afhankelijk zal zijn en nooit zo snel als gecompileerde talen met ingebouwde multi-threading.
Het is opmerkelijk dat Node.js echte multi-threading kan bereiken, op voorwaarde dat elke thread gestart wordt als een afzonderlijk proces. Hier zijn bibliotheken voor, maar Node.js heeft een ingebouwde functie genaamd Werkdraad.
Alle event loop GIF's komen van de Loep applicatie gemaakt door Philip Roberts, waarin je je asynchrone scenario's kunt testen.