Kapitel 1. Asynchronie: Jetzt und später

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Einer der wichtigsten und dennoch oft missverstandenen Teile der Programmierung in einer Sprache wie JavaScript ist die Art und Weise, wie man das Programmverhalten über einen bestimmten Zeitraum hinweg ausdrückt und manipuliert.

Dabei geht es nicht nur darum, was vom Beginn einer for Schleife bis zum Ende einer for Schleife passiert, was natürlich eine gewisse Zeit (Mikrosekunden bis Millisekunden) in Anspruch nimmt. Es geht darum, was passiert, wenn ein Teil deines Programms jetzt läuft und ein anderer Teil deines Programmsspäter - esgibt eine Lücke zwischen jetzt und später, in der dein Programm nicht aktiv ausgeführt wird.

Praktisch alle nicht-trivialen Programme, die jemals geschrieben wurden (vor allem in JS), mussten auf die eine oder andere Weise mit dieser Lücke umgehen, sei es beim Warten auf Benutzereingaben, beim Anfordern von Daten aus einer Datenbank oder einem Dateisystem, beim Senden von Daten über das Netzwerk und Warten auf eine Antwort oder beim Ausführen einer sich wiederholenden Aufgabe in einem festen Zeitintervall (z. B. Animation). Auf all diese verschiedenen Arten muss dein Programm den Zustand über die Zeitlücke hinweg verwalten. In London sagt man über die Kluft zwischen der U-Bahn-Tür und dem Bahnsteig: "mind the gap".

Die Beziehung zwischen dem jetzigen und dem späteren Teil deines Programms ist das Herzstück der asynchronen Programmierung.

Asynchrone Programmierung gibt es schon seit den Anfängen von JS, ganz klar. Aber die meisten JS-Entwicklerinnen und -Entwickler haben nie wirklich genau darüber nachgedacht, wie und warum sie in ihren Programmen auftaucht, oder verschiedene andere Möglichkeiten erkundet, damit umzugehen. Ein guter Ansatz war schon immer die bescheidene Callback-Funktion. Viele bestehen bis heute darauf, dass Rückrufe mehr als ausreichend sind.

Aber da JS sowohl im Umfang als auch in der Komplexität immer weiter wächst, um den immer größer werdenden Anforderungen einer erstklassigen Programmiersprache gerecht zu werden, die in Browsern und Servern und allen denkbaren Geräten dazwischen läuft, werden die Schmerzen, mit denen wir Asynchronität verwalten, immer lähmender und schreien nach Ansätzen, die sowohl leistungsfähiger als auch vernünftiger sind.

Auch wenn das alles im Moment noch recht abstrakt erscheint, versichere ich dir, dass wir uns im Laufe des Buches noch ausführlicher und konkreter damit befassen werden. In den nächsten Kapiteln werden wir eine Reihe von neuen Techniken für die asynchrone JavaScript-Programmierung erkunden.

Aber bevor wir das erreichen können, müssen wir erst einmal besser verstehen, was Asynchronität ist und wie sie in JS funktioniert.

Ein Programm in Stücken

Du schreibst dein JS-Programm vielleicht in einer einzigen .js-Datei, aber dein Programm besteht mit ziemlicher Sicherheit aus mehreren Chunks, von denen nur einer jetzt ausgeführt wird und der Rest später. Die häufigste Einheit jedes Chunks ist die function.

Das Problem, das die meisten Entwickler, die neu in JS sind, zu haben scheinen, ist, dass "später"nicht strikt und unmittelbar nach "jetzt" passiert. Mit anderen Worten: Aufgaben, die jetzt nicht abgeschlossen werden können, werden per Definition asynchron abgeschlossen, und daher gibt es kein blockierendes Verhalten, wie du es vielleicht intuitiv erwartest oder willst.

Bedenke:

// ajax(..) is some arbitrary Ajax function given by a library
var data = ajax( "http://some.url.1" );

console.log( data );
// Oops! `data` generally won't have the Ajax results

Du weißt wahrscheinlich, dass Standard-Ajax-Anfragen nicht synchron abgeschlossen werden, was bedeutet, dass die Funktion ajax(..) noch keinen Wert zurückgeben kann, der der Variablen data zugewiesen werden kann. Wenn ajax(..)blockieren könnte, bis die Antwort zurückkommt, dann würde die Zuweisung von data = ..gut funktionieren.

Aber das ist nicht die Art, wie wir Ajax machen. Wir stellenjetzt eine asynchrone Ajax-Anfrage und bekommen die Ergebnisse erst später zurück.

Die einfachste (aber definitiv nicht die einzige und auch nicht unbedingt die beste!) Möglichkeit, von jetzt auf später zu "warten", ist die Verwendung einer Funktion, die gemeinhin als Callback-Funktion bezeichnet wird:

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", function myCallbackFunction(data){

    console.log( data ); // Yay, I gots me some `data`!

} );
Warnung

Du hast vielleicht schon gehört, dass es möglich ist, synchrone Ajax-Anfragen zu stellen. Das ist zwar technisch gesehen richtig, aber du solltest es unter keinen Umständen tun, weil es die Benutzeroberfläche des Browsers (Schaltflächen, Menüs, Scrollen usw.) blockiert und jegliche Benutzerinteraktion verhindert. Das ist eine schreckliche Idee und sollte immer vermieden werden.

Bevor du protestierst: Nein, dein Wunsch, das Chaos der Rückrufe zu vermeiden, ist keine Rechtfertigung für blockierendes, synchrones Ajax.

Betrachte zum Beispiel diesen Code:

function now() {
    return 21;
}

function later() {
    answer = answer * 2;
    console.log( "Meaning of life:", answer );
}

var answer = now();

setTimeout( later, 1000 ); // Meaning of life: 42

Dieses Programm besteht aus zwei Teilen: dem Teil, der jetzt läuft, und dem Teil, der später läuft. Es sollte ziemlich offensichtlich sein, was diese beiden Teile sind, aber lass es uns noch einmal ganz genau sagen:

Jetzt:

function now() {
    return 21;
}

function later() { .. }

var answer = now();

setTimeout( later, 1000 );

Später:

answer = answer * 2;
console.log( "Meaning of life:", answer );

Der Now-Chunk wird sofort ausgeführt, sobald du dein Programm ausführst. setTimeout(..) legt aber auch ein Ereignis (eine Zeitüberschreitung) fest, dasspäter eintritt, sodass der Inhalt der Funktion later() zu einem späteren Zeitpunkt (1.000 Millisekunden von jetzt an) ausgeführt wird.

Jedes Mal, wenn du einen Teil deines Codes in ein function verpackst und festlegst, dass er als Reaktion auf ein Ereignis (Timer, Mausklick, Ajax-Antwort usw.) ausgeführt werden soll, erstellst du einen späteren Teil deines Codes und führst damit Asynchronität in dein Programm ein.

Async-Konsole

Es gibt keine Spezifikation oder eine Reihe von Anforderungen, wie dieconsole.* Methoden funktionieren - sie sind nicht offiziell Teil von JavaScript, sondern werden von der Hosting-Umgebung zu JS hinzugefügt (siehe den Titel Types & Grammar in dieser Serie).

Verschiedene Browser und JS-Umgebungen machen also, was sie wollen, was manchmal zu verwirrendem Verhalten führen kann.

Insbesondere gibt es einige Browser und einige Bedingungen, dieconsole.log(..) Der Hauptgrund dafür ist, dass E/A ein sehr langsamer und blockierender Teil vieler Programme ist (nicht nur JS). Daher kann es (aus Sicht der Seite/UI) besser sein, wenn ein Browser die console E/A asynchron im Hintergrund verarbeitet, ohne dass du davon etwas mitbekommst.

Ein nicht sehr häufiges, aber mögliches Szenario, bei dem diesbeobachtet werden kann (nicht vom Code selbst, sondern von außen):

var a = {
    index: 1
};

// later
console.log( a ); // ??

// even later
a.index++;

Normalerweise würden wir erwarten, dass das Objekt a genau zum Zeitpunkt der Anweisung console.log(..) geknipst wird und etwas wie{ index: 1 } ausgibt, so dass in der nächsten Anweisung, wenn a.index++ passiert, etwas anderes als oder genau nach der Ausgabe von a geändert wird.

In den meisten Fällen wird der vorangehende Code wahrscheinlich eine Objektdarstellung in der Konsole deiner Entwicklertools erzeugen, wie du sie erwarten würdest. Es ist jedoch möglich, dass derselbe Code in einer Situation ausgeführt wird, in der der Browser meint, die Konsolen-E/A in den Hintergrund verschieben zu müssen. In diesem Fall ist es möglich, dass zu dem Zeitpunkt, an dem das Objekt in der Konsole des Browsers dargestellt wird, die a.index++ bereits geschehen ist und { index: 2 } angezeigt wird.

Es ist ungewiss, unter welchen Bedingungen genau console E/A aufgeschoben wird oder ob sie überhaupt beobachtbar ist. Sei dir dieser möglichen Asynchronität der E/A bewusst, falls du beimDebuggen auf Problemestößt, bei denen Objekte nach einer console.log(..) Anweisung geändert wurden und du trotzdem die unerwarteten Änderungen bemerkst.

Hinweis

Wenn du in dieses seltene Szenario gerätst, ist die beste Option, Breakpoints in deinem JS-Debugger zu verwenden, anstatt dich auf die Ausgabe von console zu verlassen. Die nächstbeste Option wäre, einen "Schnappschuss" des fraglichen Objekts zu erzwingen, indem du es in eine string serialisierst, wie mitJSON.stringify(..).

Ereignisschleife

Stellen wir eine (vielleicht schockierende) Behauptung auf: Obwohl du eindeutig in der Lage bist, asynchronen JS-Code zu schreiben (wie die Zeitüberschreitung, die wir uns gerade angesehen haben), hatte JavaScript selbst bis vor Kurzem (ES6) noch nie eine direkte Vorstellung von Asynchronität.

Was!? Das scheint eine verrückte Behauptung zu sein, oder? Tatsächlich ist es so: Die JS-Engine selbst hat noch nie etwas anderes getan, als einen einzelnen Teil deines Programms auszuführen, wenn sie dazu aufgefordert wurde.

"Aufgefordert". Von wem? Das ist der wichtige Teil!

Die JS-Engine läuft nicht in Isolation. Sie läuft in einer Hosting-Umgebung, die für die meisten Entwickler der typische Webbrowser ist. In den letzten Jahren (aber keineswegs nur) hat sich JS über den Browser hinaus auf andere Umgebungen wie Server ausgeweitet, z. B. über Node.js. Heutzutage wird JavaScript in alle möglichen Geräte eingebaut, von Robotern bis hin zu Glühbirnen.

Aber all diese Umgebungen haben eines gemeinsam: Sie verfügen über einen Mechanismus, mit dem du mehrere Teile deines Programmsim Laufe der Zeit ausführen kannst, indem du jedes Mal die JS-Engine aufrufst, die sogenannte Ereignisschleife.

Mit anderen Worten: Die JS-Engine hatte kein angeborenes Zeitgefühl, sondern war stattdessen eine Ausführungsumgebung auf Abruf für beliebige JS-Schnipsel. Es ist die Umgebung, die immer "Ereignisse" (JS-Code-Ausführungen)geplant hat.

Wenn dein JS-Programm zum Beispiel eine Ajax-Anfrage stellt, um Daten von einem Server zu holen, legst du den Antwortcode in einer Funktion fest (die gemeinhin als Callback bezeichnet wird), und die JS-Engine teilt der Hosting-Umgebung mit: "Hey, ich werde die Ausführung für den Moment aussetzen, aber sobald du mit dieser Netzwerkanfrage fertig bist und Daten hast, rufe bitte diese Funktion zurück."

Der Browser wird dann so eingerichtet, dass er auf die Antwort aus dem Netzwerk wartet. Wenn er dir etwas zu sagen hat, plant er die Ausführung der Callback-Funktion, indem er sie in die Ereignisschleife einfügt.

Was ist also die Ereignisschleife?

Stellen wir uns das Ganze zunächst anhand eines gefälschten Codes vor:

// `eventLoop` is an array that acts as a queue
// (first-in, first-out)
var eventLoop = [ ];
var event;

// keep going "forever"
while (true) {
    // perform a "tick"
    if (eventLoop.length > 0) {
        // get the next event in the queue
        event = eventLoop.shift();

        // now, execute the next event
        try {
            event();
        }
        catch (err) {
            reportError(err);
        }
    }
}

Dies ist natürlich ein stark vereinfachter Pseudocode, um die Konzepte zu veranschaulichen. Aber es sollte reichen, um ein besseres Verständnis zu bekommen.

Wie du siehst, gibt es eine fortlaufende Schleife, die durch diewhile Schleife dargestellt wird, und jede Iteration dieser Schleife wird als Tick bezeichnet. Wenn bei jedem Tick ein Ereignis in der Warteschlange wartet, wird es entnommen und ausgeführt. Diese Ereignisse sind deine Funktionsrückrufe.

Es ist wichtig zu wissen, dass setTimeout(..) deinen Callback nicht in die Warteschlange der Ereignisschleife stellt. Wenn der Timer abläuft, setzt die Umgebung deinen Callback in die Ereignisschleife, so dass er von einem zukünftigen Tick abgeholt und ausgeführt wird.

Was ist, wenn sich zu diesem Zeitpunkt bereits 20 Elemente in der Ereignisschleife befinden? Dein Callback wartet. Er reiht sich hinter den anderen ein - normalerweise gibt es keinen Weg, der Warteschlange zuvorzukommen und sich vorzudrängeln. Das erklärt, warum die Zeitgeber von setTimeout(..) möglicherweise nicht mit perfekter zeitlicher Genauigkeit ausgelöst werden. Du hast (grob gesagt) die Garantie, dass dein Callback nicht vor dem von dir angegebenen Zeitintervall ausgelöst wird, aber je nach Zustand der Ereigniswarteschlange kann es zu diesem Zeitpunkt oder danach passieren.

Mit anderen Worten: Dein Programm ist in der Regel in viele kleine Abschnitte unterteilt, die nacheinander in der Warteschlange der Ereignisschleife ablaufen. Und technisch gesehen können auch andere Ereignisse, die nicht direkt mit deinem Programm zu tun haben, in die Warteschlange eingefügt werden.

Hinweis

Wir haben "bis vor kurzem" erwähnt, weil ES6 die Art der Verwaltung der Warteschleife für die Ereignisschleife geändert hat. Das ist vor allem eine formale Formalität, aber ES6 legt jetzt genau fest, wie die Ereignisschleife funktioniert, was bedeutet, dass sie technisch gesehen in den Zuständigkeitsbereich der JS-Engine fällt und nicht nur in den der Hosting-Umgebung. Ein Hauptgrund für diese Änderung ist die Einführung von ES6 Promises, die wir in Kapitel 3 besprechen werden, weil sie eine direkte, feinkörnige Kontrolle über die Zeitplanungsprogramme in der Warteschleife der Ereignisschleife erfordern (siehe die Diskussion von setTimeout(..0) in "Zusammenarbeit").

Paralleles Gewindeschneiden

Häufig werden die Begriffe "asynchron" und "parallel" miteinander verwechselt, aber sie sind eigentlich ganz unterschiedlich. Erinnere dich: Bei asynchron geht es um die Lücke zwischenjetzt und später. Bei parallel geht es darum, dass Dinge gleichzeitig passieren können.

Die gebräuchlichsten Werkzeuge für paralleles Rechnen sind Prozesse und Threads. Prozesse und Threads werden unabhängig voneinander ausgeführt und können gleichzeitig ausgeführt werden: auf getrennten Prozessoren oder sogar getrennten Computern, aber mehrere Threads können sich den Speicher eines einzelnen Prozesses teilen.

Eine Ereignisschleife hingegen teilt ihre Arbeit in Aufgaben auf und führt sie seriell aus, so dass der parallele Zugriff auf den gemeinsamen Speicher und Änderungen daran nicht möglich sind. Parallelität und Serialität können in Form von kooperierenden Ereignisschleifen in separaten Threads nebeneinander bestehen.

Die Verschachtelung von parallelen Ausführungssträngen und die Verschachtelung von asynchronen Ereignissen finden auf sehr unterschiedlichen Granularitätsebenen statt.

Zum Beispiel:

function later() {
    answer = answer * 2;
    console.log( "Meaning of life:", answer );
}

Während der gesamte Inhalt von later() als ein einziger Eintrag in der Warteschlange der Ereignisschleife betrachtet werden könnte, gibt es in einem Thread, in dem dieser Code ausgeführt wird, in Wirklichkeit vielleicht ein Dutzend verschiedener Low-Level-Operationen. Zum Beispiel muss für answer = answer * 2 zuerst der aktuelle Wert von answer geladen werden, dann wird 2 irgendwo abgelegt, dann wird die Multiplikation durchgeführt, dann wird das Ergebnis genommen und wieder inanswer gespeichert.

In einer Single-Thread-Umgebung spielt es keine Rolle, dass die Elemente in der Thread-Warteschlange Low-Level-Operationen sind, da nichts den Thread unterbrechen kann. Wenn du aber ein paralleles System hast, in dem zwei verschiedene Threads im selben Programm arbeiten, kann es sehr wahrscheinlich zu unvorhersehbarem Verhalten kommen.

Bedenke:

var a = 20;

function foo() {
    a = a + 1;
}

function bar() {
    a = a * 2;
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

Im Single-Thread-Verhalten von JavaScript, wenn foo() vorbar() läuft, ist das Ergebnis, dass a 42 hat, aber wenn bar() vorfoo() läuft, wird das Ergebnis in a 41 sein.

Wenn JS-Ereignisse, die dieselben Daten teilen, parallel ausgeführt würden, wären die Probleme jedoch viel subtiler. Betrachte diese beiden Listen von Pseudocode-Tasks als die Threads, die den Code auffoo() bzw. bar() ausführen könnten, und überlege, was passiert, wenn sie genau zur gleichen Zeit ausgeführt werden:

Thread 1 (X und Y sind temporäre Speicherplätze):

foo():
  a. load value of `a` in `X`
  b. store `1` in `Y`
  c. add `X` and `Y`, store result in `X`
  d. store value of `X` in `a`

Thread 2 (X und Y sind temporäre Speicherplätze):

bar():
  a. load value of `a` in `X`
  b. store `2` in `Y`
  c. multiply `X` and `Y`, store result in `X`
  d. store value of `X` in `a`

Nehmen wir nun an, dass die beiden Threads wirklich parallel laufen. Du kannst das Problem wahrscheinlich schon erkennen, oder? Sie verwenden die gemeinsamen SpeicherplätzeX und Y für ihre temporären Schritte.

Was ist das Endergebnis in a, wenn die Schritte so ablaufen?

1a  (load value of `a` in `X`   ==> `20`)
2a  (load value of `a` in `X`   ==> `20`)
1b  (store `1` in `Y`   ==> `1`)
2b  (store `2` in `Y`   ==> `2`)
1c  (add `X` and `Y`, store result in `X`   ==> `22`)
1d  (store value of `X` in `a`   ==> `22`)
2c  (multiply `X` and `Y`, store result in `X`   ==> `44`)
2d  (store value of `X` in `a`   ==> `44`)

Das Ergebnis in a wird 44 sein. Aber was ist mit dieser Reihenfolge?

1a  (load value of `a` in `X`   ==> `20`)
2a  (load value of `a` in `X`   ==> `20`)
2b  (store `2` in `Y`   ==> `2`)
1b  (store `1` in `Y`   ==> `1`)
2c  (multiply `X` and `Y`, store result in `X`   ==> `20`)
1c  (add `X` and `Y`, store result in `X`   ==> `21`)
1d  (store value of `X` in `a`   ==> `21`)
2d  (store value of `X` in `a`   ==> `21`)

Das Ergebnis auf a ist 21.

Die Programmierung mit Threads ist also sehr knifflig, denn wenn du keine besonderen Maßnahmen ergreifst, um diese Art von Unterbrechung/Verschachtelung zu verhindern, kann es zu sehr überraschendem, nicht-deterministischem Verhalten kommen, das häufig zu Kopfschmerzen führt.

JavaScript teilt niemals Daten über Threads hinweg, was bedeutet, dass dieser Grad an Nicht-Determinismus kein Problem darstellt. Aber das bedeutet nicht, dass JS immer deterministisch ist. Erinnerst du dich daran, dass die relative Reihenfolge von foo()und bar() zu zwei verschiedenen Ergebnissen führt (41 oder 42)?

Hinweis

Es ist vielleicht noch nicht offensichtlich, aber nicht jeder Nicht-Determinismus ist schlecht. Manchmal ist er irrelevant, und manchmal ist er beabsichtigt. In diesem und den nächsten Kapiteln werden wir mehr Beispiele dafür sehen.

Run-to-Completion

Aufgrund des Single-Threading von JavaScript ist der Code in foo()(und bar()) atomar, d.h. sobald foo() ausgeführt wird, wird der gesamte Code beendet, bevor der Code in bar()ausgeführt werden kann, und umgekehrt. Das nennt man Run-to-Completion-Verhalten.

Tatsächlich wird die Run-to-Completion-Semantik deutlicher, wenn foo()und bar() mehr Code enthalten, wie z.B.:

var a = 1;
var b = 2;

function foo() {
    a++;
    b = b * a;
    a = b + 3;
}

function bar() {
    b--;
    a = 8 + b;
    b = a * 2;
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

Da foo() nicht von bar() und bar() nicht von foo() unterbrochen werden kann, hat dieses Programm nur zwei mögliche Ergebnisse, je nachdem, welches zuerst ausgeführt wird. Wenn es Threading gäbe und die einzelnen Anweisungen in foo() und bar() verschachtelt werden könnten, würde sich die Anzahl der möglichen Ergebnisse stark erhöhen!

Chunk 1 ist synchron (passiert jetzt), aber Chunks 2 und 3 sind asynchron (passieren später), was bedeutet, dass ihre Ausführung durch eine Zeitspanne getrennt ist.

Chunk 1:

var a = 1;
var b = 2;

Chunk 2 (foo()):

a++;
b = b * a;
a = b + 3;

Chunk 3 (bar()):

b--;
a = 8 + b;
b = a * 2;

Die Abschnitte 2 und 3 können in der Reihenfolge "entweder-oder" passieren, daher gibt es zwei mögliche Ergebnisse für dieses Programm, wie hier gezeigt:

Ergebnis 1:

var a = 1;
var b = 2;

// foo()
a++;
b = b * a;
a = b + 3;

// bar()
b--;
a = 8 + b;
b = a * 2;

a; // 11
b; // 22

Ergebnis 2:

var a = 1;
var b = 2;

// bar()
b--;
a = 8 + b;
b = a * 2;

// foo()
a++;
b = b * a;
a = b + 3;

a; // 183
b; // 180

Zwei Ergebnisse desselben Codes bedeuten, dass wir immer noch Unbestimmtheit haben! Allerdings auf der Ebene der Funktions-(Ereignis-)ordnung und nicht auf der Ebene der Anweisungsordnung (oder sogar auf der Ebene der Ausdrucksoperationen), wie es bei Threads der Fall ist. Mit anderen Worten: Es ist deterministischer als es bei Threads der Fall gewesen wäre.

Auf das Verhalten von JavaScript angewandt, wird diese Unbestimmtheit der Funktionsreihenfolge als Race Condition bezeichnet, da foo() undbar() gegeneinander antreten, um zu sehen, wer zuerst ausgeführt wird. Genauer gesagt ist es eine Race Condition, weil du nicht zuverlässig vorhersagen kannst, wie a und b ausfallen werden.

Hinweis

Wenn es in JS eine Funktion gäbe, die nicht bis zum Abschluss ausgeführt werden könnte, hätten wir viel mehr Möglichkeiten, oder? Mit ES6 wurde genau so etwas eingeführt (siehe Kapitel 4), aber keine Sorge, darauf kommen wir noch zurück!

Gleichzeitigkeit

Stellen wir uns eine Website vor, die eine Liste von Statusmeldungen anzeigt (wie ein Newsfeed in einem sozialen Netzwerk), die nach und nach geladen wird, wenn der Nutzer die Liste nach unten scrollt. Damit eine solche Funktion korrekt funktioniert, müssen (mindestens) zwei getrennte "Prozesse" gleichzeitig ausgeführt werden (d. h. während desselben Zeitfensters, aber nicht unbedingt zum selben Zeitpunkt).

Hinweis

Wir verwenden den Begriff "Prozess" hier in Anführungszeichen, weil es sich nicht um echte Prozesse auf Betriebssystemebene im Sinne der Informatik handelt. Es handelt sich um virtuelle Prozesse oder Tasks, die eine logisch zusammenhängende, sequentielle Reihe von Operationen darstellen. Wir verwenden den Begriff "Prozess" anstelle von "Aufgabe", weil er terminologisch zu den Definitionen der Konzepte passt, die wir erforschen.

Der erste "Prozess" reagiert auf onscroll Ereignisse (Ajax-Anfragen für neue Inhalte), die ausgelöst werden, wenn der Nutzer die Seite weiter nach unten gescrollt hat. Der zweite "Prozess" empfängt Ajax-Antworten (um Inhalte auf der Seite zu rendern).

Wenn ein Benutzer schnell genug scrollt, kann es natürlich sein, dass in der Zeit, in der die erste Antwort zurückbekommen und verarbeitet wird, zwei oder mehronscroll -Ereignisse ausgelöst werden. Daher werden onscroll-Ereignisse und Ajax-Antwort-Ereignisse schnell ausgelöst und ineinander verschachtelt.

Von Gleichzeitigkeit spricht man, wenn zwei oder mehr "Prozesse" gleichzeitig im selben Zeitraum ausgeführt werden, unabhängig davon, ob die einzelnen Operationen, aus denen sie bestehen, parallel ablaufen (zum selben Zeitpunkt auf verschiedenen Prozessoren oder Kernen). Du kannst dir Gleichzeitigkeit also als Parallelität auf "Prozess"-Ebene (oder Task-Ebene) vorstellen, im Gegensatz zur Parallelität auf Vorgangsebene (Threads mit getrennten Prozessoren).

Hinweis

Die Gleichzeitigkeit führt auch einen optionalen Begriff für diese "Prozesse" ein, die miteinander interagieren. Wir werden später darauf zurückkommen.

Für ein bestimmtes Zeitfenster (ein paar Sekunden, in denen ein Benutzer scrollt) können wir uns jeden unabhängigen "Prozess" als eine Reihe von Ereignissen/Operationen vorstellen:

"Prozess" 1 (onscroll events):

onscroll, request 1
onscroll, request 2
onscroll, request 3
onscroll, request 4
onscroll, request 5
onscroll, request 6
onscroll, request 7

"Verarbeiten" 2 (Ajax-Antwort-Ereignisse):

response 1
response 2
response 3
response 4
response 5
response 6
response 7

Es ist durchaus möglich, dass ein onscroll -Ereignis und ein Ajax-Antwort-Ereignis genau zum selben Zeitpunkt verarbeitet werden können. Lass uns diese Ereignisse zum Beispiel in einer Zeitleiste visualisieren:

onscroll, request 1
onscroll, request 2          response 1
onscroll, request 3          response 2
response 3
onscroll, request 4
onscroll, request 5
onscroll, request 6          response 4
onscroll, request 7
response 6
response 5
response 7

Aber um auf unseren Begriff der Ereignisschleife von vorhin zurückzukommen: JS kann immer nur ein Ereignis auf einmal verarbeiten, d.h. entweder passiert onscroll, request 2 zuerst oder response 1 zuerst, aber beides kann nicht buchstäblich im selben Moment passieren. Genau wie bei den Kindern in der Schulkantine: Egal, welche Menschenmenge sich vor den Türen bildet, sie müssen sich in einer einzigen Schlange aufstellen, um ihr Mittagessen zu bekommen!

Lass uns die Verschachtelung all dieser Ereignisse in der Warteschlange der Ereignisschleife veranschaulichen:

onscroll, request 1   <--- Process 1 starts
onscroll, request 2
response 1            <--- Process 2 starts
onscroll, request 3
response 2
response 3
onscroll, request 4
onscroll, request 5
onscroll, request 6
response 4
onscroll, request 7   <--- Process 1 finishes
response 6
response 5
response 7            <--- Process 2 finishes

"Prozess" 1 und "Prozess" 2 laufen gleichzeitig (aufgabenspezifisch parallel), aber ihre einzelnen Ereignisse laufen nacheinander in der Warteschlange der Ereignisschleife.

Übrigens, ist dir aufgefallen, dass response 6 und response 5 nicht in der erwarteten Reihenfolge zurückkamen?

Die Single-Thread-Ereignisschleife ist ein Ausdruck von Gleichzeitigkeit (es gibt sicherlich noch andere, auf die wir später zurückkommen).

Nicht interagierend

Da zwei oder mehr "Prozesse" ihre Schritte/Ereignisse innerhalb desselben Programms gleichzeitig ablaufen lassen, müssen sie nicht unbedingt miteinander interagieren, wenn die Aufgaben nicht miteinander verbunden sind. Wenn sie nicht miteinander interagieren, ist Nondeterminismus völlig in Ordnung.

Zum Beispiel:

var res = {};

function foo(results) {
    res.foo = results;
}

function bar(results) {
    res.bar = results;
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

foo() und bar() sind zwei gleichzeitige "Prozesse", und es ist unbestimmt, in welcher Reihenfolge sie ausgelöst werden. Aber wir haben das Programm so konstruiert, dass es keine Rolle spielt, in welcher Reihenfolge sie ausgelöst werden, weil sie unabhängig voneinander agieren und daher nicht miteinander interagieren müssen.

Dies ist kein Race-Condition-Fehler, da der Code unabhängig von der Reihenfolge immer korrekt funktioniert.

Interaktion

In der Regel interagieren konkurrierende "Prozesse" indirekt über den Bereich und/oder das DOM. Wenn es zu solchen Interaktionen kommt, musst du diese koordinieren, um Race Conditions zu verhindern, wie oben beschrieben.

Hier ist ein einfaches Beispiel für zwei gleichzeitige "Prozesse", die aufgrund einer impliziten Reihenfolge interagieren, die nur manchmal unterbrochen wird:

var res = [];

function response(data) {
    res.push( data );
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

Die gleichzeitigen "Prozesse" sind die beiden response() Aufrufe, die gemacht werden, um die Ajax-Antworten zu verarbeiten. Sie können in der Reihenfolge "either-first" erfolgen.

Nehmen wir an, das erwartete Verhalten ist, dass res[0] die Ergebnisse des Aufrufs "http://some.url.1" und res[1] die Ergebnisse des Aufrufs"http://some.url.2" erhält. Manchmal ist das der Fall, aber manchmal sind sie vertauscht, je nachdem, welcher Aufruf zuerst beendet wird. Es ist ziemlich wahrscheinlich, dass es sich bei dieser Unbestimmtheit um einen Race Condition Bug handelt.

Hinweis

Sei sehr vorsichtig mit Annahmen, zu denen du in solchen Situationen neigen könntest. Es ist zum Beispiel nicht ungewöhnlich, dass ein Entwickler feststellt, dass "http://some.url.2" immer viel langsamer antwortet als"http://some.url.1", vielleicht aufgrund der Aufgaben, die sie erledigen (z. B. führt der eine eine Datenbankaufgabe aus und der andere holt nur eine statische Datei), so dass die beobachtete Reihenfolge immer wie erwartet zu sein scheint. Selbst wenn beide Anfragen an denselben Server gehen und dieser absichtlich in einer bestimmten Reihenfolge antwortet, gibt es keine wirkliche Garantie dafür, in welcher Reihenfolge die Antworten im Browser zurückkommen.

Um eine solche Wettlaufsituation zu vermeiden, kannst du die Interaktion bei der Bestellung koordinieren:

var res = [];

function response(data) {
    if (data.url == "http://some.url.1") {
        res[0] = data;
    }
    else if (data.url == "http://some.url.2") {
        res[1] = data;
    }
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

Unabhängig davon, welche Ajax-Antwort zuerst zurückkommt, prüfen wir diedata.url (vorausgesetzt, es kommt eine vom Server zurück!), um herauszufinden, welche Position die Antwortdaten im Array reseinnehmen sollen. res[0] enthält immer die "http://some.url.1" -Ergebnisse undres[1] enthält immer die "http://some.url.2" -Ergebnisse. Durch eine einfache Koordination haben wir den Nondeterminismus der Race Condition beseitigt.

Die gleichen Überlegungen würden auch gelten, wenn mehrere gleichzeitige Funktionsaufrufe über das gemeinsame DOM miteinander interagieren würden, z. B. wenn einer den Inhalt von <div> und der andere den Stil oder die Attribute von <div> aktualisiert (z. B. um das DOM-Element sichtbar zu machen, sobald es einen Inhalt hat). Du würdest das DOM-Element wahrscheinlich nicht anzeigen wollen, bevor es einen Inhalt hat, also muss die Koordination die richtige Reihenfolge der Interaktion sicherstellen.

Manche Gleichzeitigkeitsszenarien sind ohne koordinierte Interaktion immer (nicht nur manchmal) kaputt. Bedenke:

var a, b;

function foo(x) {
    a = x * 2;
    baz();
}

function bar(y) {
    b = y * 2;
    baz();
}

function baz() {
    console.log(a + b);
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

In diesem Beispiel wird, egal ob foo() oder bar() zuerst ausgelöst wird, immer baz() zu früh ausgeführt (entweder a oder b wird immer nochundefined), aber der zweite Aufruf von baz() wird funktionieren, da sowohla als auch b verfügbar sind.

Es gibt verschiedene Möglichkeiten, eine solche Situation anzugehen. Hier ist eine einfache Möglichkeit:

var a, b;

function foo(x) {
    a = x * 2;
    if (a && b) {
        baz();
    }
}

function bar(y) {
    b = y * 2;
    if (a && b) {
        baz();
    }
}

function baz() {
    console.log( a + b );
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

Die if (a && b) Bedingung um den baz() Aufruf wird traditionell als Tor bezeichnet, weil wir nicht sicher sind, in welcher Reihenfolge a und b ankommen werden, aber wir warten darauf, dass beide ankommen, bevor wir das Tor öffnen ( baz() aufrufen).

Eine weitere Gleichzeitigkeitsbedingung, auf die du stoßen könntest, wird manchmal als Wettlauf bezeichnet, aber korrekter ist die Bezeichnung " Latch". Es ist gekennzeichnet durch das Verhalten "nur der Erste gewinnt". In diesem Fall ist Nondeterminismus akzeptabel, da du ausdrücklich sagst, dass es in Ordnung ist, wenn das "Rennen" bis zur Ziellinie nur einen Gewinner hat.

Betrachte diesen kaputten Code:

var a;

function foo(x) {
    a = x * 2;
    baz();
}

function bar(x) {
    a = x / 2;
    baz();
}

function baz() {
    console.log( a );
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

Je nachdem, welcher von beiden (foo() oder bar()) zuletzt ausgelöst wird, wird nicht nur der zugewiesene Wert von a überschrieben, sondern es wird auch der Aufruf von baz() dupliziert (was wahrscheinlich unerwünscht ist).

So können wir die Interaktion mit einer einfachen Verriegelung koordinieren, um nur den ersten durchzulassen:

var a;

function foo(x) {
    if (!a) {
        a = x * 2;
        baz();
    }
}

function bar(x) {
    if (!a) {
        a = x / 2;
        baz();
    }
}

function baz() {
    console.log( a );
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

Die Bedingung if (!a) lässt nur den ersten von foo() oder bar()durch, und der zweite (und alle folgenden) Aufrufe würden einfach ignoriert werden. Es ist einfach nicht gut, an zweiter Stelle zu stehen!

Hinweis

In all diesen Szenarien haben wir zur Veranschaulichung globale Variablen verwendet, aber unsere Überlegungen erfordern dies nicht. Solange die fraglichen Funktionen auf die Variablen zugreifen können (über den Scope), funktionieren sie wie vorgesehen. Die Abhängigkeit von lexikalisch zugewiesenen Variablen (siehe den Titel Scope & Closures in dieser Reihe) und sogar von globalen Variablen wie in diesen Beispielen ist ein offensichtlicher Nachteil dieser Formen der Gleichzeitigkeitskoordination. In den nächsten Kapiteln werden wir andere Möglichkeiten der Koordination kennenlernen, die in dieser Hinsicht viel sauberer sind.

Zusammenarbeit

Eine andere Form der Koordination von Gleichzeitigkeit ist die kooperative Gleichzeitigkeit. Hier liegt der Schwerpunkt nicht so sehr auf der Interaktion durch die gemeinsame Nutzung von Werten in Bereichen (obwohl das natürlich auch erlaubt ist!). Das Ziel ist es, einen lang laufenden "Prozess" in Schritte oder Stapel aufzuteilen, damit andere gleichzeitige "Prozesse" die Möglichkeit haben, ihre Operationen in die Warteschleife der Ereignisschleife einzuschleusen.

Nehmen wir zum Beispiel einen Ajax Response Handler, der eine lange Liste von Ergebnissen durchlaufen muss, um die Werte umzuwandeln. Wir verwendenArray#map(..), um den Code kürzer zu halten:

var res = [];

// `response(..)` receives array of results from the Ajax call
function response(data) {
    // add onto existing `res` array
    res = res.concat(
        // make a new transformed array with all
        // `data` values doubled
        data.map( function(val){
            return val * 2;
        } )
    );
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

Wenn "http://some.url.1" seine Ergebnisse zuerst zurückbekommt, wird die gesamte Liste auf einmal auf res abgebildet. Wenn es sich um ein paar tausend oder weniger Datensätze handelt, ist das in der Regel keine große Sache. Aber wenn es sich um 10 Millionen Datensätze handelt, kann das eine Weile dauern (mehrere Sekunden auf einem leistungsstarken Laptop, viel länger auf einem mobilen Gerät usw.).

Während ein solcher "Prozess" läuft, kann nichts anderes auf der Seite passieren, auch keine anderen response(..) Aufrufe, keine UI-Aktualisierungen, nicht einmal Benutzerereignisse wie Scrollen, Tippen, Klicken auf eine Schaltfläche und Ähnliches. Das ist ziemlich schmerzhaft.

Um also ein kooperatives, konkurrierendes System zu schaffen, das freundlicher ist und die Warteschlange der Ereignisschleife nicht in Anspruch nimmt, kannst du diese Ergebnisse in asynchronen Stapeln verarbeiten und nach jedem Stapel wieder in die Ereignisschleife zurückkehren, damit andere wartende Ereignisse stattfinden können.

Hier ist ein sehr einfacher Ansatz:

var res = [];

// `response(..)` receives array of results from the Ajax call
function response(data) {
    // let's just do 1000 at a time
    var chunk = data.splice( 0, 1000 );

    // add onto existing `res` array
    res = res.concat(
        // make a new transformed array with all
        // `chunk` values doubled
        chunk.map( function(val){
            return val * 2;
        } )
    );

    // anything left to process?
    if (data.length > 0) {
        // async schedule next batch
        setTimeout( function(){
            response( data );
        }, 0 );
    }
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

Wir verarbeiten den Datensatz in maximal 1.000 Datenpunkten. Auf diese Weise stellen wir sicher, dass der "Prozess" kurz ist, auch wenn das viele weitere "Prozesse" nach sich zieht, denn durch die Einbindung in die Warteschleife der Ereignisschleife erhalten wir eine viel reaktionsschnellere (performantere) Website/App.

Natürlich koordinieren wir die Reihenfolge dieser "Prozesse" nicht durch Interaktion, sodass die Reihenfolge der Ergebnisse auf res nicht vorhersehbar ist. Wenn eine Reihenfolge erforderlich wäre, müsstest du Interaktionstechniken verwenden, wie wir sie bereits besprochen haben oder wie wir sie in späteren Kapiteln dieses Buches behandeln werden.

Wir verwenden das setTimeout(..0) (Hack) für asynchrones Zeitplannungsprogramm, was im Grunde genommen nur bedeutet: "Hänge diese Funktion an das Ende der aktuellen Warteschlange der Ereignisschleife."

Hinweis

setTimeout(..0) ist technisch gesehen kein direktes Einfügen eines Ereignisses in die Warteschlange der Ereignisschleife. Der Timer fügt das Ereignis bei seiner nächsten Gelegenheit ein. Bei zwei aufeinanderfolgenden Aufrufen von setTimeout(..0) ist zum Beispiel nicht unbedingt gewährleistet, dass sie in der Reihenfolge der Aufrufe abgearbeitet werden. In Node.js gibt es einen ähnlichen Ansatz:process.nextTick(..). Auch wenn es praktisch (und in der Regel performanter) wäre, gibt es (zumindest bisher) keinen direkten Weg, um in allen Umgebungen eine asynchrone Ereignisreihenfolge zu gewährleisten. Auf dieses Thema gehen wir im nächsten Abschnitt näher ein.

Aufträge

Seit ES6 gibt es ein neues Konzept, das über der Warteschleife der Ereignisschleife liegt: die Auftragswarteschleife. Am ehesten kommst du mit ihr durch das asynchrone Verhalten von Promises in Berührung (siehe Kapitel 3).

Leider handelt es sich im Moment um einen Mechanismus ohne offengelegte API, so dass es etwas komplizierter ist, ihn zu demonstrieren. Daher werden wir ihn konzeptionell beschreiben, damit du, wenn wir in Kapitel 3 das asynchrone Verhalten mit Promises besprechen, verstehst, wie diese Aktionen geplant und verarbeitet werden.

Ich habe herausgefunden, dass man sich das am besten so vorstellen kann, dass die Auftragswarteschlange eine Warteschlange ist, die am Ende jedes Ticks in der Warteschlange der Ereignisschleife hängt. Bestimmte asynchrone Aktionen, die während eines Ticks der Ereignisschleife auftreten können, führen nicht dazu, dass ein ganz neues Ereignis in die Warteschlange der Ereignisschleife aufgenommen wird, sondern fügen stattdessen ein Element (auch Auftrag genannt) am Ende der Auftragswarteschlange des aktuellen Ticks hinzu.

Das ist so, als würde man sagen: "Oh, hier ist noch eine Sache, die ichspäter erledigen muss, aber sie muss sofort erledigt werden, bevor etwas anderes passieren kann.

Die Warteschlange in der Ereignisschleife ist wie eine Fahrt im Vergnügungspark: Wenn du die Fahrt beendet hast, musst du dich ans Ende der Schlange stellen, um erneut zu fahren. Bei der Job-Warteschlange ist es so, als ob du die Fahrt beendest, dich anstellst und gleich wieder einsteigst.

Ein Auftrag kann auch dazu führen, dass weitere Aufträge am Ende der gleichen Warteschlange hinzugefügt werden. Theoretisch ist es also möglich, dass eine Auftragsschleife (ein Auftrag, der immer wieder neue Aufträge hinzufügt usw.) endlos weiterläuft und das Programm so die Möglichkeit verliert, zum nächsten Tick der Ereignisschleife überzugehen. Das wäre konzeptionell fast dasselbe wie eine lang laufende oder unendliche Schleife (wie while (true) ..) in deinem Code.

Aufträge sind so etwas wie der Geist des setTimeout(..0) hack, aber so umgesetzt, dass sie eine viel klarere und garantierte Reihenfolge haben: später, aber so bald wie möglich.

Stellen wir uns ein Zeitplannungsprogramm für Aufträge vor (direkt, ohne Hacks) und nennen es schedule(..). Überlege dir das:

console.log( "A" );

setTimeout( function(){
    console.log( "B" );
}, 0 );

// theoretical "Job API"
schedule( function(){
    console.log( "C" );

    schedule( function(){
        console.log( "D" );
    } );
} );

Du könntest erwarten, dass dies A B C D ausgibt, aber stattdessen würde es A C D B ausgeben, weil die Aufträge am Ende des aktuellen Ticks der Ereignisschleife stattfinden und der Timer ausgelöst wird, um für den nächsten Tick der Ereignisschleife zu planen (falls verfügbar!).

In Kapitel 3 werden wir sehen, dass das asynchrone Verhalten von Promises auf Aufträgen basiert, daher ist es wichtig, dass du dir klar machst, wie das mit dem Verhalten von Ereignisschleifen zusammenhängt.

Ausweisbestellung

Die Reihenfolge, in der wir Anweisungen in unserem Code ausdrücken, ist nicht unbedingt die gleiche, in der die JS-Engine sie ausführt. Diese Behauptung mag seltsam klingen, deshalb werden wir sie kurz erläutern.

Doch bevor wir das tun, sollten wir uns über etwas im Klaren sein: Die Regeln/Grammatik der Sprache (siehe den Titel Typen & Grammatik dieser Serie) diktieren ein sehr vorhersehbares und zuverlässiges Verhalten für die Anweisungsreihenfolge aus der Sicht des Programms. Was wir jetzt besprechen werden, sind also Dinge, die du in deinem JS-Programm niemals beobachten solltest.

Warnung

Wenn du jemals in der Lage bist, die Umordnung von Compiler-Anweisungen zu beobachten, wie wir es hier zeigen, wäre das ein klarer Verstoß gegen die Spezifikation und würde zweifellos auf einen Fehler in der betreffenden JS-Engine zurückzuführen sein - ein Fehler, der umgehend gemeldet und behoben werden sollte! Aber es ist viel häufiger, dass du vermutest, dass etwas Verrücktes in der JS-Engine passiert, obwohl es in Wirklichkeit nur ein Fehler (wahrscheinlich eine Race Condition!) in deinem eigenen Code ist - also sieh dort zuerst nach, und das immer wieder. Der JS-Debugger, der Haltepunkte verwendet und den Code Zeile für Zeile durchgeht, ist dein mächtigstes Werkzeug, um solche Fehler in deinem Code aufzuspüren.

Bedenke:

var a, b;

a = 10;
b = 30;

a = a + 1;
b = b + 1;

console.log( a + b ); // 42

In diesem Code gibt es keine ausdrückliche Asynchronität (abgesehen von der seltenenconsole asynchrone E/A, die wir bereits besprochen haben!), also ist die wahrscheinlichste Annahme, dass er Zeile für Zeile in einer Top-down-Methode verarbeitet wird.

Aber es ist möglich, dass die JS-Engine nach dem Kompilieren dieses Codes (ja, JS wird kompiliert - siehe den Titel dieser Serie über Scope & Closures!) Möglichkeiten findet, deinen Code schneller auszuführen, indem sie die Reihenfolge dieser Anweisungen (sicher) umstellt. Solange du die Umordnung nicht beobachten kannst, ist im Grunde alles erlaubt.

Die Engine könnte zum Beispiel feststellen, dass es schneller ist, den Code wie folgt auszuführen:

var a, b;

a = 10;
a++;

b = 30;
b++;

console.log( a + b ); // 42

Oder dies:

var a, b;

a = 11;
b = 31;

console.log( a + b ); // 42

Oder sogar:

// because `a` and `b` aren't used anymore, we can
// inline and don't even need them!
console.log( 42 ); // 42

In all diesen Fällen führt die JS-Engine während der Kompilierung sichere Optimierungen durch, da das beobachtbare Endergebnis dasselbe ist.

Aber hier ist ein Szenario, in dem diese speziellen Optimierungen unsicher wären und daher nicht erlaubt werden könnten (was natürlich nicht bedeutet, dass es überhaupt nicht optimiert ist):

var a, b;

a = 10;
b = 30;

// we need `a` and `b` in their preincremented state!
console.log( a * b ); // 300

a = a + 1;
b = b + 1;

console.log( a + b ); // 42

Andere Beispiele, bei denen die Neuordnung durch den Compiler zu beobachtbaren Seiteneffekten führen könnte (und daher verboten werden muss), sind z. B. Funktionsaufrufe mit Seiteneffekten (auch und vor allem Getter-Funktionen) oder ES6-Proxy-Objekte (siehe den Titel ES6 & Beyond dieser Serie).

Bedenke:

function foo() {
    console.log( b );
    return 1;
}

var a, b, c;

// ES5.1 getter literal syntax
c = {
    get bar() {
        console.log( a );
        return 1;
    }
};

a = 10;
b = 30;

a += foo();             // 30
b += c.bar;             // 11

console.log( a + b );   // 42

Ohne die console.log(..) Anweisungen in diesem Schnipsel (die nur zur Veranschaulichung als bequeme Form eines beobachtbaren Nebeneffekts verwendet werden), hätte die JS-Engine den Code wahrscheinlich neu anordnen können, wenn sie gewollt hätte (wer weiß, ob sie das getan hätte!?):

// ...

a = 10 + foo();
b = 30 + c.bar;

// ...

Obwohl uns die JS-Semantik glücklicherweise vor den beobachtbarenAlbträumen schützt, die bei der Neuordnung von Compiler-Anweisungen drohen, ist es dennoch wichtig zu verstehen, wie eng die Verbindung zwischen der Art und Weise, wie der Quellcode (von oben nach unten) erstellt wird, und der Art und Weise, wie er nach der Kompilierung läuft, ist.

Die Umordnung von Compiler-Anweisungen ist fast eine Mikro-Metapher für Gleichzeitigkeit und Interaktion. Als allgemeines Konzept kann dir dieses Bewusstsein helfen, Probleme mit asynchronem JS-Codefluss besser zu verstehen.

Überprüfung

Ein JavaScript-Programm ist (praktisch) immer in zwei oder mehr Chunks unterteilt, wobei der erste Chunk jetzt ausgeführt wird und der nächste Chunkspäter, als Reaktion auf ein Ereignis. Auch wenn das Programm Chunk für Chunk ausgeführt wird, haben alle Chunks den gleichen Zugriff auf den Programmbereichund den Status, sodass jede Änderung des Status auf dem vorherigen Status aufbaut.

Immer wenn es Ereignisse gibt, die ausgeführt werden müssen, läuft die Ereignisschleife, bis die Warteschlange leer ist. Jede Iteration der Ereignisschleife ist ein Tick. Benutzerinteraktionen, IO und Timer reihen Ereignisse in die Warteschlange ein.

Zu einem bestimmten Zeitpunkt kann immer nur ein Ereignis aus der Warteschlange verarbeitet werden. Während ein Ereignis ausgeführt wird, kann es direkt oder indirekt ein oder mehrere nachfolgende Ereignisse auslösen.

Von Gleichzeitigkeit spricht man, wenn zwei oder mehr Ereignisketten zeitlich ineinandergreifen, so dass es aus einer übergeordneten Perspektive so aussieht, als würden sie gleichzeitig ablaufen (auch wenn zu einem bestimmten Zeitpunkt nur ein Ereignis verarbeitet wird).

Oft ist es notwendig, die Interaktion zwischen diesen gleichzeitigen "Prozessen" (im Gegensatz zu Betriebssystemprozessen) zu koordinieren, um z. B. die Reihenfolge sicherzustellen oder Wettlaufbedingungen zu verhindern. Diese "Prozesse" können auch zusammenarbeiten, indem sie sich in kleinere Teile aufteilen und andere "Prozesse" ineinander greifen lassen.

Get Du kennst JS nicht: Asynchronität und Leistung now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.