Die Ereignisschleife und Rückruffunktionen verstehen

0 Aktien
0
0
0
0

Einführung

In den Anfängen des Internets bestanden Websites oft aus statischen Daten auf einer HTML-Seite. Da Webanwendungen jedoch interaktiver und dynamischer geworden sind, ist es zunehmend notwendig, rechenintensive Operationen durchzuführen, wie beispielsweise externe Netzwerkanfragen zum Abrufen von API-Daten. Um diese Operationen in JavaScript auszuführen, muss ein Entwickler asynchrone Programmiertechniken anwenden.

Da JavaScript eine Single-Thread-Programmiersprache mit einem nebenläufigen Ausführungsmodell ist, das Operationen nacheinander verarbeitet, kann sie immer nur einen Befehl gleichzeitig bearbeiten. Eine Aktion wie das Anfordern von Daten von einer API kann jedoch je nach Größe der angeforderten Daten, Geschwindigkeit der Netzwerkverbindung und anderen Faktoren eine unbestimmte Zeit in Anspruch nehmen. Würden API-Aufrufe synchron erfolgen, könnte der Browser keine Benutzereingaben wie Scrollen oder Klicken auf eine Schaltfläche verarbeiten, bis diese Operation abgeschlossen ist. Dies wird als Blockierung bezeichnet.

Um Blockierungen zu vermeiden, bietet die Browserumgebung zahlreiche asynchrone Web-APIs, auf die JavaScript zugreifen kann. Das bedeutet, dass diese APIs parallel zu anderen Operationen anstatt sequenziell ausgeführt werden können. Dies ist vorteilhaft, da der Benutzer den Browser während der Verarbeitung der asynchronen Operation wie gewohnt weiter nutzen kann.

Ereignisschleife

Dieser Abschnitt erklärt, wie JavaScript asynchronen Code mithilfe der Ereignisschleife verarbeitet. Zunächst wird die Funktionsweise der Ereignisschleife anhand einer Demonstration veranschaulicht, anschließend werden zwei ihrer Elemente erläutert: der Stack und die Queue.

JavaScript-Code, der keine der asynchronen Web-APIs verwendet, wird synchron ausgeführt – nacheinander und sequenziell. Dies wird durch folgenden Beispielcode veranschaulicht, der drei Funktionen aufruft, von denen jede eine Zahl in der Konsole ausgibt:

// Define three example functions
function first() {
console.log(1)
}

function second() {
console.log(2)
}

function third() {
console.log(3)
}

In diesem Code definieren Sie drei Funktionen, die Zahlen mit console.log() ausgeben.

Schreiben Sie anschließend die Funktionsaufrufe:

// Execute the functions
first()
second()
third()

Die Ausgabe basiert auf der Reihenfolge, in der die Funktionen aufgerufen werden – first(), second(), dann three():

Output
1
2
3

Bei der Verwendung einer asynchronen Web-API werden die Regeln komplexer. Eine integrierte API, mit der Sie experimentieren können, ist `setTimeout`. Diese Funktion setzt einen Timer und führt nach einer festgelegten Zeitspanne eine Aktion aus. `setTimeout` muss asynchron sein, da sonst der gesamte Browser während des Wartens einfriert, was zu einer schlechten Benutzererfahrung führt.

Um eine asynchrone Anfrage zu simulieren, fügen Sie der zweiten Funktion setTimeout hinzu:

// Define three example functions, but one of them contains asynchronous code
function first() {
console.log(1)
}

function second() {
setTimeout(() => {
console.log(2)
}, 0)
}

function third() {
console.log(3)
}

`setTimeout` benötigt zwei Argumente: eine asynchron auszuführende Funktion und die Wartezeit vor dem Aufruf dieser Funktion. In diesem Codebeispiel wird `console.log` in eine anonyme Funktion ausgelagert und an `setTimeout` übergeben. Anschließend wird die Funktion so konfiguriert, dass sie nach 0 Millisekunden ausgeführt wird.

Rufen Sie nun die Funktionen wie zuvor auf:

// Execute the functions
first()
second()
third()

Man könnte erwarten, dass die drei Funktionen die Zahlen weiterhin in der richtigen Reihenfolge ausgeben, wenn setTimeout auf 0 gesetzt wird. Da die Ausführung jedoch asynchron erfolgt, wird die letzte Zahl mit einem Break-Befehl ausgegeben.

Output
1
3
2

Ob Sie den Timer auf null Sekunden oder fünf Minuten einstellen, spielt keine Rolle – `console.log`, das von asynchronem Code aufgerufen wird, wird erst nach den synchronen Funktionen der obersten Ebene ausgeführt. Dies liegt daran, dass die JavaScript-Hostumgebung, in diesem Fall der Browser, die sogenannte Ereignisschleife verwendet, um gleichzeitige oder parallele Ereignisse zu verarbeiten. Da JavaScript immer nur einen Befehl gleichzeitig ausführen kann, benötigt es die Ereignisschleife, um zu wissen, wann ein bestimmter Befehl ausgeführt wird. Die Ereignisschleife realisiert dies mithilfe von Stapeln und Warteschlangen.

Stapel

Der Stack, auch Aufrufstapel genannt, speichert den Zustand der aktuell ausgeführten Funktion. Falls Ihnen das Konzept eines Stacks noch nicht geläufig ist, können Sie ihn sich wie ein Array mit dem LIFO-Prinzip (Last In, First Out) vorstellen. Das bedeutet, dass Sie nur Elemente vom unteren Ende des Stacks hinzufügen oder entfernen können. JavaScript führt den aktuellen Frame (bzw. den aktuellen Funktionsaufruf in einer bestimmten Umgebung) auf dem Stack aus, entfernt ihn anschließend und fährt mit dem nächsten Frame fort.

Bei einem Beispiel, das ausschließlich synchronen Code enthält, führt der Browser diesen in folgender Reihenfolge aus:

  • Erste() Füge die Funktion zum Stack hinzu, führe die Funktion first() aus, die 1 in der Konsole ausgibt, und entferne anschließend first() vom Stack.
  • zweite() Füge die Funktion dem Stapel hinzu, führe die Funktion second() aus, die 2 auf der Konsole ausgibt, und entferne die Funktion second() vom Stapel.
  • dritte () Füge es dem Stack hinzu, führe third() aus, das 3 in der Konsole ausgibt, und entferne third() vom Stack.

Das zweite Beispiel mit setTimeout sieht wie folgt aus:

  • Erste() Füge die Funktion zum Stack hinzu, führe die Funktion first() aus, die 1 in der Konsole ausgibt, und entferne anschließend first() vom Stack.
  • zweite() Füge etwas zum Stack hinzu und führe second() aus.
    • Füge setTimeout() zum Stack hinzu, führe die Web-API-Funktion setTimeout() aus, die den Timer startet und die anonyme Funktion zur Warteschlange hinzufügt, und entferne setTimeout() vom Stack.
  • zweite() Vom Stapel entfernen.
  • Dritte () Füge es dem Stack hinzu, führe third() aus, das 3 in der Konsole ausgibt, und entferne third() vom Stack.
  • Die Ereignisschleife prüft die Warteschlange auf ausstehende Nachrichten, findet die anonyme Funktion aus setTimeout(), fügt die Funktion dem Stapel hinzu, die 2 in der Konsole protokolliert, und entfernt sie dann vom Stapel.

Mit setTimeout führt eine asynchrone Web-API das Konzept einer Warteschlange ein, das in diesem Tutorial später behandelt wird.

Warteschlange

Eine Warteschlange, auch Nachrichtenwarteschlange oder Aufgabenwarteschlange genannt, ist ein Wartebereich für Funktionen. Wenn der Aufrufstapel leer ist, prüft die Ereignisschleife die Warteschlange auf ausstehende Nachrichten, beginnend mit der ältesten. Wird eine gefunden, fügt sie diese dem Stapel hinzu, woraufhin die in der Nachricht enthaltene Funktion ausgeführt wird.

Im Beispiel mit `setTimeout` wird die anonyme Funktion unmittelbar nach der restlichen Ausführung des Hauptprogramms ausgeführt, da der Timer auf 0 Sekunden gesetzt wurde. Wichtig ist, dass der Timer nicht bedeutet, dass der Code exakt bei 0 Sekunden oder zu einem bestimmten Zeitpunkt ausgeführt wird, sondern dass die anonyme Funktion während dieser Zeit in die Warteschlange eingereiht wird. Dieses Warteschlangensystem ist notwendig, da die Ausführung der aktuell ausgeführten Funktion unterbrochen werden würde, wenn der Timer die anonyme Funktion nach Ablauf des Timers direkt auf den Stack legen würde. Dies könnte unbeabsichtigte und unvorhersehbare Folgen haben.

Hinweis: Es gibt außerdem eine weitere Warteschlange, die sogenannte Job- oder Mikrotask-Warteschlange, die Promises verarbeitet. Mikrotasks wie Promises werden mit höherer Priorität ausgeführt als Makrotasks wie setTimeout.

Nachdem Sie nun wissen, wie die Ereignisschleife mithilfe von Stack und Queue die Ausführungsreihenfolge des Codes steuert, geht es im nächsten Schritt darum, die Ausführungsreihenfolge in Ihrem Code zu kontrollieren. Dazu lernen Sie zunächst die wichtigste Methode kennen, um sicherzustellen, dass die Ereignisschleife asynchronen Code korrekt verarbeitet: Callback-Funktionen.

Callback-Funktionen

Im Beispiel mit `setTimeout` wird die Funktion mit dem Timeout erst nach allen anderen Funktionen im Hauptkontext der obersten Ausführungsebene ausgeführt. Um jedoch sicherzustellen, dass eine der Funktionen, beispielsweise die dritte Funktion, nach dem Timeout ausgeführt wird, müssen Sie asynchrone Programmiertechniken verwenden. Das Timeout kann hier einen asynchronen API-Aufruf repräsentieren, der Daten liefert. Sie möchten mit den Daten aus dem API-Aufruf arbeiten, müssen aber zunächst sicherstellen, dass die Daten zurückgegeben wurden.

Die Hauptlösung für dieses Problem besteht in der Verwendung von Callback-Funktionen. Callback-Funktionen haben keine spezielle Syntax. Sie sind einfach Funktionen, die als Argument an eine andere Funktion übergeben werden. Eine Funktion, die eine andere Funktion als Argument akzeptiert, wird als Funktion höherer Ordnung bezeichnet. Gemäß dieser Definition kann jede Funktion aufgerufen werden, wenn sie als Argument übergeben wird. Telefongespräche sind zwar nicht von Natur aus asynchron, können aber für asynchrone Zwecke genutzt werden.

Hier ist ein Syntaxbeispiel für eine Funktion höherer Ordnung und einen Callback:

// A function
function fn() {
console.log('Just a function')
}

// A function that takes another function as an argument
function higherOrderFunction(callback) {
// When you call a function that is passed as an argument, it is referred to as a callback
callback()
}

// Passing a function
higherOrderFunction(fn)

In diesem Code definieren Sie eine Funktion fn, definieren eine Funktion aboveOrderFunction, die eine Callback-Funktion als Argument entgegennimmt, und übergeben fn als Callback an aboveOrderFunction.

Die Ausführung dieses Codes bewirkt Folgendes:

Output
Just a function

Kehren wir zu den ersten, zweiten und dritten Funktionen mit setTimeout zurück. Hier ist der bisherige Stand:

function first() {
console.log(1)
}

function second() {
setTimeout(() => {
console.log(2)
}, 0)
}

function third() {
console.log(3)
}

Die Aufgabe besteht darin, die dritte Funktion so zu steuern, dass ihre Ausführung immer so lange verzögert wird, bis die asynchrone Aktion in der zweiten Funktion abgeschlossen ist. Hier kommen Callback-Funktionen ins Spiel. Anstatt die erste, zweite und dritte Funktion jeweils auf oberster Ebene auszuführen, wird die dritte Funktion als Argument an die zweite übergeben. Die zweite Funktion führt den Callback aus, nachdem die asynchrone Aktion abgeschlossen ist.

Hier sind drei Funktionen, die mit einer Callback-Funktion implementiert wurden:

// Define three functions
function first() {
console.log(1)
}

function second(callback) {
setTimeout(() => {
console.log(2)

// Execute the callback function
callback()
}, 0)
}

function third() {
console.log(3)
}

Führe nun den ersten und zweiten Schritt aus und übergib anschließend den dritten Schritt als Argument an den zweiten:

first()
second(third)

Nach Ausführung dieses Codeblocks erhalten Sie folgende Ausgabe:

Output
1
2
3

Zuerst wird 1 ausgegeben, und nachdem der Timer abgelaufen ist (in diesem Fall null Sekunden, aber Sie können ihn auf einen beliebigen Wert ändern), wird 2 ausgegeben, und dann 3. Arbeiten Sie, bis die asynchrone Web-API (setTimeout) abgeschlossen ist.

Der entscheidende Punkt ist, dass Callback-Funktionen nicht asynchron sind – `setTimeout` ist eine asynchrone Web-API, die für die Verarbeitung asynchroner Aufgaben zuständig ist. Callbacks ermöglichen es lediglich, benachrichtigt zu werden, wenn eine asynchrone Aufgabe abgeschlossen ist, und den Erfolg oder Misserfolg dieser Aufgabe zu verarbeiten.

Nachdem Sie nun gelernt haben, wie man Callbacks zur Ausführung asynchroner Aufgaben verwendet, erklärt der nächste Abschnitt die Probleme, die mit der Verschachtelung zu vieler Aufrufe und der Entstehung einer “Pyramide des Verderbens” verbunden sind.

Verschachtelter Rückruf und Pyramide des Schreckens

Callback-Funktionen sind eine effektive Methode, um sicherzustellen, dass eine Funktion so lange verzögert wird, bis eine andere Funktion abgeschlossen ist und Daten zurückgibt. Aufgrund der verschachtelten Struktur von Callbacks kann der Code jedoch schnell unübersichtlich werden, wenn viele aufeinanderfolgende asynchrone Anfragen voneinander abhängen. Dies war für frühe JavaScript-Entwickler ein großes Ärgernis, und so wird Code mit verschachtelten Aufrufen oft als “Pyramide der Qualen” oder “Callback-Hölle” bezeichnet.

Hier ist eine Demonstration verschachtelter Rückruffunktionen:

function pyramidOfDoom() {
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
}, 500)
}, 2000)
}, 1000)
}

In diesem Code wird jeder neue setTimeout-Aufruf in eine Funktion höherer Ordnung eingebettet, wodurch eine Pyramide immer tieferer Aufrufe entsteht. Die Ausführung dieses Codes führt zu folgendem Ergebnis:

Output
1
2
3

In der Praxis, mit realem asynchronem Code, kann dies deutlich komplexer werden. Sie müssen wahrscheinlich Fehler im asynchronen Code behandeln und anschließend Daten aus jeder Antwort an die nächste Anfrage weitergeben. Die Verwendung von Callbacks erschwert die Lesbarkeit und Wartung Ihres Codes.

Hier ist ein praktikables Beispiel für eine realistischere “Pyramide des Verderbens”, mit der Sie experimentieren können:

// Example asynchronous function
function asynchronousRequest(args, callback) {
// Throw an error if no arguments are passed
if (!args) {
return callback(new Error('Whoa! Something went wrong.'))
} else {
return setTimeout(
// Just adding in a random number so it seems like the contrived asynchronous function
// returned different data
() => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
500,
)
}
}
// Nested asynchronous requests
function callbackHell() {
asynchronousRequest('First', function first(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest('Second', function second(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest(null, function third(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
})
})
})
}
// Execute 
callbackHell()

In diesem Code muss für jede Funktion eine mögliche Antwort und einen möglichen Fehler berücksichtigt werden, was die CallbackHell-Funktion optisch verwirrend macht.

Die Ausführung dieses Codes liefert folgendes Ergebnis:

Output
First 9
Second 3
Error: Whoa! Something went wrong.
at asynchronousRequest (<anonymous>:4:21)
at second (<anonymous>:29:7)
at <anonymous>:9:13

Ergebnis

Diese Methode zur Verwaltung asynchronen Codes ist schwierig. Daher wurde in ES6 das Konzept der Promises eingeführt. Dies ist der Schwerpunkt des nächsten Abschnitts.

Ein Kommentar
Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Das könnte Ihnen auch gefallen