Verständnis der Ereignisschleife, Rückrufe, Promises und Async/Await in JavaScript

Der Autor hat den COVID-19-Hilfsfonds ausgewählt, um eine Spende im Rahmen des Write for Donations-Programms zu erhalten.

Einführung

In den Anfangstagen des Internets bestanden Websites oft aus statischen Daten in einer HTML-Seite. Da jedoch Webanwendungen interaktiver und dynamischer geworden sind, ist es zunehmend erforderlich geworden, intensive Operationen wie das Herstellen externer Netzwerkanfragen zur Abfrage von API-Daten durchzuführen. Um diese Operationen in JavaScript zu behandeln, muss ein Entwickler asynchrone Programmierung-Techniken verwenden.

Da JavaScript eine single-threaded-Programmiersprache mit einem synchronen Ausführungsmodell ist, das eine Operation nach der anderen verarbeitet, kann es nur eine Anweisung gleichzeitig verarbeiten. Eine Aktion wie das Anfordern von Daten aus einer API kann jedoch eine unbestimmte Zeit in Anspruch nehmen, abhängig von der Größe der angeforderten Daten, der Geschwindigkeit der Netzwerkverbindung und anderen Faktoren. Wenn API-Aufrufe synchron durchgeführt würden, könnte der Browser keine Benutzereingaben wie Scrollen oder Klicken auf eine Schaltfläche verarbeiten, bis diese Operation abgeschlossen ist. Dies wird als Blocking bezeichnet.

Um blockierendes Verhalten zu verhindern, verfügt die Browserumgebung über viele Web-APIs, auf die JavaScript zugreifen kann, die asynchron sind, was bedeutet, dass sie parallel zu anderen Operationen ausgeführt werden können, anstatt sequenziell. Dies ist nützlich, weil es dem Benutzer ermöglicht, den Browser normal zu verwenden, während die asynchronen Operationen verarbeitet werden.

Als JavaScript-Entwickler müssen Sie wissen, wie Sie mit asynchronen Web-APIs arbeiten und auf die Antwort oder den Fehler dieser Operationen reagieren. In diesem Artikel erfahren Sie mehr über die Ereignisschleife, die ursprüngliche Methode zum Umgang mit asynchronem Verhalten durch Rückrufe, die aktualisierte ECMAScript 2015-Ergänzung von Versprechen und die moderne Praxis, async/await zu verwenden.

Hinweis: Dieser Artikel konzentriert sich auf clientseitiges JavaScript in der Browserumgebung. Die gleichen Konzepte gelten im Allgemeinen auch in der Node.js-Umgebung, jedoch verwendet Node.js eigene C++-APIs anstelle der Web-APIs des Browsers. Für weitere Informationen zur asynchronen Programmierung in Node.js werfen Sie einen Blick auf How To Write Asynchronous Code in Node.js.

Die Ereignisschleife

Dieser Abschnitt wird erklären, wie JavaScript asynchronen Code mit der Ereignisschleife verarbeitet. Zunächst wird eine Demonstration der Arbeitsweise der Ereignisschleife durchgeführt und dann die beiden Elemente der Ereignisschleife erläutert: der Stapel und die Warteschlange.

JavaScript-Code, der keine asynchronen Web-APIs verwendet, wird synchron ausgeführt – einer nach dem anderen, sequenziell. Dies wird durch diesen Beispielcode demonstriert, der drei Funktionen aufruft, von denen jede eine Zahl in die Konsole druckt:

// Definiere drei Beispiel-Funktionen
function first() {
  console.log(1)
}

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

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

In diesem Code werden drei Funktionen definiert, die Zahlen mit console.log() ausgeben.

Anschließend rufen Sie die Funktionen auf:

// Führe die Funktionen aus
first()
second()
third()

Die Ausgabe erfolgt basierend auf der Reihenfolge, in der die Funktionen aufgerufen wurden – erstens(), zweitens() und dann drittens():

Output
1 2 3

Wenn eine asynchrone Web-API verwendet wird, werden die Regeln komplizierter. Eine integrierte API, mit der Sie dies testen können, ist setTimeout, die einen Timer setzt und eine Aktion nach einer festgelegten Zeitspanne ausführt. setTimeout muss asynchron sein, da sonst der gesamte Browser während des Wartens eingefroren bleiben würde, was zu einer schlechten Benutzererfahrung führen würde.

Fügen Sie setTimeout zur Funktion zweitens hinzu, um eine asynchrone Anfrage zu simulieren:

// Definiere drei Beispiel-Funktionen, wobei eine davon asynchronen Code enthält
function first() {
  console.log(1)
}

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

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

setTimeout nimmt zwei Argumente: die Funktion, die es asynchron ausführen wird, und die Zeit, die es warten wird, bevor es diese Funktion aufruft. In diesem Code haben Sie console.log in eine anonyme Funktion eingeschlossen und sie an setTimeout übergeben, dann die Funktion so eingestellt, dass sie nach 0 Millisekunden ausgeführt wird.

Rufen Sie nun die Funktionen auf, wie Sie es zuvor getan haben:

// Die Funktionen ausführen
first()
second()
third()

Man könnte erwarten, dass bei einem setTimeout von 0 das Ausführen dieser drei Funktionen immer noch dazu führt, dass die Zahlen in sequenzieller Reihenfolge gedruckt werden. Aber weil es asynchron ist, wird die Funktion mit dem Timeout zuletzt gedruckt:

Output
1 3 2

Ob Sie den Timeout auf null Sekunden oder fünf Minuten setzen, macht keinen Unterschied – das von asynchronem Code aufgerufene console.log wird nach den synchronen Top-Level-Funktionen ausgeführt. Dies geschieht, weil die JavaScript-Hostumgebung, in diesem Fall der Browser, ein Konzept namens die Ereignisschleife verwendet, um Parallelität oder parallele Ereignisse zu behandeln. Da JavaScript immer nur eine Anweisung gleichzeitig ausführen kann, muss die Ereignisschleife darüber informiert werden, wann welche spezifische Anweisung ausgeführt werden soll. Die Ereignisschleife behandelt dies mit den Konzepten eines Stapels und einer Warteschlange.

Stapel

Der Stack oder Aufrufstapel speichert den Zustand der aktuell ausgeführten Funktion. Wenn Sie mit dem Konzept eines Stapels nicht vertraut sind, können Sie ihn sich als Array mit „Last In, First Out“ (LIFO)-Eigenschaften vorstellen, was bedeutet, dass Sie nur Elemente am Ende des Stapels hinzufügen oder entfernen können. JavaScript führt den aktuellen Rahmen (oder Funktionsaufruf in einer bestimmten Umgebung) im Stapel aus, entfernt ihn und fährt mit dem nächsten fort.

Für das Beispiel, das nur synchronen Code enthält, behandelt der Browser die Ausführung in folgender Reihenfolge:

  • Fügen Sie first() dem Stapel hinzu, führen Sie first() aus, das 1 in die Konsole protokolliert, und entfernen Sie first() aus dem Stapel.
  • Fügen Sie second() dem Stapel hinzu, führen Sie second() aus, das 2 in die Konsole protokolliert, und entfernen Sie second() aus dem Stapel.
  • Fügen Sie third() dem Stapel hinzu, führen Sie third() aus, das 3 in die Konsole protokolliert, und entfernen Sie third() aus dem Stapel.

Das zweite Beispiel mit setTimeout sieht so aus:

  • Fügen Sie first() dem Stapel hinzu, führen Sie first() aus, das 1 in die Konsole protokolliert, und entfernen Sie first() aus dem Stapel.
  • Fügen Sie second() dem Stapel hinzu, führen Sie second() aus.
    • Fügen Sie setTimeout() dem Stapel hinzu, führen Sie die setTimeout() Web-API aus, die einen Timer startet und die anonyme Funktion zur Warteschlange hinzufügt, und entfernen Sie setTimeout() aus dem Stapel.
  • Entfernen Sie second() vom Stack.
  • Fügen Sie third() zum Stack hinzu, führen Sie third() aus, das 3 in die Konsole protokolliert, und entfernen Sie third() vom Stack.
  • Die Ereignisschleife überprüft die Warteschlange auf ausstehende Nachrichten und findet die anonyme Funktion von setTimeout(), fügt die Funktion zum Stack hinzu, die 2 in die Konsole protokolliert, und entfernt sie dann vom Stack.

Mit setTimeout, einer asynchronen Web-API, wird das Konzept der Warteschlange eingeführt, das in diesem Tutorial als nächstes behandelt wird.

Warteschlange

Die Warteschlange, auch als Nachrichtenwarteschlange oder Aufgabenwarteschlange bezeichnet, ist ein Wartebereich für Funktionen. Wenn der Aufrufstapel leer ist, überprüft die Ereignisschleife die Warteschlange auf ausstehende Nachrichten, beginnend mit der ältesten Nachricht. Sobald sie eine findet, fügt sie sie dem Stack hinzu, der die Funktion in der Nachricht ausführt.

Im setTimeout-Beispiel wird die anonyme Funktion sofort nach dem Rest der Ausführung auf oberster Ebene ausgeführt, da der Timer auf 0 Sekunden gesetzt wurde. Es ist wichtig zu bedenken, dass der Timer nicht bedeutet, dass der Code genau nach 0 Sekunden oder nach der angegebenen Zeit ausgeführt wird, sondern dass die anonyme Funktion nach dieser Zeitspanne zur Warteschlange hinzugefügt wird. Dieses Warteschlangensystem existiert, weil wenn der Timer die anonyme Funktion direkt auf den Stack hinzufügen würde, wenn der Timer endet, würde es die aktuell laufende Funktion unterbrechen, was unbeabsichtigte und unvorhersehbare Auswirkungen haben könnte.

Hinweis: Es gibt auch eine weitere Warteschlange namens Aufgabenwarteschlange oder Mikroaufgabenwarteschlange, die Promises behandelt. Mikroaufgaben wie Promises werden mit höherer Priorität behandelt als Makroaufgaben wie setTimeout.

Jetzt wissen Sie, wie die Ereignisschleife den Stapel und die Warteschlange verwendet, um die Ausführungsreihenfolge des Codes zu behandeln. Die nächste Aufgabe besteht darin, herauszufinden, wie die Ausführungsreihenfolge im Code gesteuert werden kann. Dazu werden Sie zunächst die ursprüngliche Methode kennenlernen, um sicherzustellen, dass asynchroner Code von der Ereignisschleife korrekt behandelt wird: Callback-Funktionen.

Callback-Funktionen

Im Beispiel mit setTimeout wurde die Funktion mit dem Timeout nach allem im Hauptausführungskontext ausgeführt. Aber wenn Sie sicherstellen möchten, dass eine der Funktionen, wie die Funktion dritte, nach dem Timeout ausgeführt wird, müssen Sie asynchrone Codierungsmethoden verwenden. Der Timeout hier kann einen asynchronen API-Aufruf darstellen, der Daten enthält. Sie möchten mit den Daten vom API-Aufruf arbeiten, müssen aber sicherstellen, dass die Daten zuerst zurückgegeben werden.

Die ursprüngliche Lösung für dieses Problem besteht darin, Rückruffunktionen zu verwenden. Rückruffunktionen haben keine spezielle Syntax; sie sind nur eine Funktion, die als Argument an eine andere Funktion übergeben wurde. Die Funktion, die eine andere Funktion als Argument akzeptiert, wird als Höher-Ordnung-Funktion bezeichnet. Gemäß dieser Definition kann jede Funktion eine Rückruffunktion werden, wenn sie als Argument übergeben wird. Rückrufe sind von Natur aus nicht asynchron, können aber für asynchrone Zwecke verwendet werden.

Hier ist ein syntaktisches Codebeispiel einer Höher-Ordnung-Funktion und eines Rückrufs:

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

// Eine Funktion, die eine andere Funktion als Argument akzeptiert
function higherOrderFunction(callback) {
  // Wenn Sie eine Funktion aufrufen, die als Argument übergeben wird, wird dies als Rückruf bezeichnet
  callback()
}

// Übergeben einer Funktion
higherOrderFunction(fn)

In diesem Code definieren Sie eine Funktion fn, definieren eine Funktion höherOrdnungsFunktion, die eine Funktion rückruf als Argument akzeptiert, und übergeben fn als Rückruf an höherOrdnungsFunktion.

Die Ausführung dieses Codes wird Folgendes ergeben:

Output
Just a function

Kehren wir zu den ersten, zweiten und dritten Funktionen mit setTimeout zurück. Dies ist, was Sie bisher haben:

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

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

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

Die Aufgabe besteht darin, die Ausführung der dritten Funktion immer zu verzögern, bis die asynchrone Aktion in der zweiten Funktion abgeschlossen ist. Hier kommen Rückruf-Funktionen ins Spiel. Anstatt erste, zweite und dritte auf der obersten Ebene der Ausführung auszuführen, werden Sie die dritte Funktion als Argument an zweite übergeben. Die zweite Funktion wird den Rückruf nach Abschluss der asynchronen Aktion ausführen.

Hier sind die drei Funktionen mit einem Rückruf angewendet:

// Definieren Sie drei Funktionen
function first() {
  console.log(1)
}

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

    // Führen Sie die Rückruffunktion aus
    callback()
  }, 0)
}

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

Jetzt führen Sie erste und zweite aus, und übergeben Sie dann dritte als Argument an zweite:

first()
second(third)

Nach Ausführung dieses Codeblocks erhalten Sie die folgende Ausgabe:

Output
1 2 3

Zuerst wird 1 gedruckt, und nach Abschluss des Timers (in diesem Fall null Sekunden, aber Sie können ihn auf eine beliebige Menge ändern) wird 2 dann 3 gedruckt. Indem Sie eine Funktion als Rückruf übergeben, haben Sie die Ausführung der Funktion erfolgreich verzögert, bis die asynchrone Web-API (setTimeout) abgeschlossen ist.

Der entscheidende Punkt hier ist, dass Rückruffunktionen nicht asynchron sind – setTimeout ist die asynchrone Web-API, die für die Behandlung asynchroner Aufgaben verantwortlich ist. Der Rückruf ermöglicht es lediglich, informiert zu werden, wenn eine asynchrone Aufgabe abgeschlossen ist, und behandelt den Erfolg oder das Scheitern der Aufgabe.

Jetzt, da Sie gelernt haben, wie Sie Rückrufe verwenden, um asynchrone Aufgaben zu behandeln, erklärt der nächste Abschnitt die Probleme des Verschachtelns zu vieler Rückrufe und das Erstellen einer „Pyramide des Grauens“.

Verschachtelte Rückrufe und die Pyramide des Grauens

Rückruffunktionen sind eine effektive Möglichkeit, die verzögerte Ausführung einer Funktion zu gewährleisten, bis eine andere abgeschlossen ist und Daten zurückgibt. Aufgrund der verschachtelten Struktur von Rückrufen kann der Code jedoch unübersichtlich werden, wenn Sie viele aufeinanderfolgende asynchrone Anfragen haben, die voneinander abhängen. Dies war für JavaScript-Entwickler früher eine große Frustration, und daher wird Code, der verschachtelte Rückrufe enthält, oft als „Pyramide des Grauens“ oder „Rückrufhölle“ bezeichnet.

Hier ist eine Demonstration verschachtelter Rückrufe:

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

In diesem Code ist jedes neue setTimeout in eine höhere Funktion verschachtelt, wodurch eine Pyramidenform mit immer tieferen Rückrufen entsteht. Das Ausführen dieses Codes würde Folgendes ergeben:

Output
1 2 3

In der Praxis kann dies bei echtzeit Asynchroncode viel komplizierter werden. Wahrscheinlich müssen Sie Fehlerbehandlungen im asynchronen Code durchführen und dann Daten von jeder Antwort an die nächste Anfrage übergeben. Dies mit Rückrufen zu tun, wird Ihren Code schwer lesbar und wartbar machen.

Hier ist ein ausführbares Beispiel für einen realistischeren „Pyramiden-der-Verzweiflung“-Code, mit dem Sie herumspielen können:

// Beispiel für asynchrone Funktion
function asynchronousRequest(args, callback) {
  // Werfen Sie einen Fehler, wenn keine Argumente übergeben werden
  if (!args) {
    return callback(new Error('Whoa! Something went wrong.'))
  } else {
    return setTimeout(
      // Fügen Sie einfach eine Zufallszahl hinzu, damit es so aussieht, als ob die konstruierte asynchrone Funktion
      // unterschiedliche Daten zurückgibt
      () => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
      500,
    )
  }
}

// Verschachtelte asynchrone Anfragen
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)
      })
    })
  })
}

// Ausführen 
callbackHell()

In diesem Code müssen Sie jede Funktion auf eine mögliche response und einen möglichen Fehler vorbereiten, was die Funktion callbackHell visuell verwirrend macht.

Das Ausführen dieses Codes ergibt Folgendes:

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

Diese Art der Behandlung von asynchronem Code ist schwer nachvollziehbar. Daher wurde das Konzept der Versprechen in ES6 eingeführt. Dies ist der Schwerpunkt des nächsten Abschnitts.

Versprechen

A promise represents the completion of an asynchronous function. It is an object that might return a value in the future. It accomplishes the same basic goal as a callback function, but with many additional features and a more readable syntax. As a JavaScript developer, you will likely spend more time consuming promises than creating them, as it is usually asynchronous Web APIs that return a promise for the developer to consume. This tutorial will show you how to do both.

Erstellen eines Versprechens

Sie können ein Versprechen mit der Syntax new Promise initialisieren, und Sie müssen es mit einer Funktion initialisieren. Die Funktion, die einem Versprechen übergeben wird, hat die Parameter resolve und reject. Die Funktionen resolve und reject behandeln jeweils den Erfolg und das Scheitern einer Operation.

Schreiben Sie die folgende Zeile, um ein Versprechen zu deklarieren:

// Ein Versprechen initialisieren
const promise = new Promise((resolve, reject) => {})

Wenn Sie das initialisierte Versprechen in diesem Zustand mit der Konsole Ihres Webbrowsers inspizieren, werden Sie feststellen, dass es den Status ausstehend und den Wert undefined hat:

Output
__proto__: Promise [[PromiseStatus]]: "pending" [[PromiseValue]]: undefined

Bisher wurde nichts für das Versprechen eingerichtet, daher wird es für immer in einem ausstehenden Zustand verbleiben. Das erste, was Sie tun können, um ein Versprechen zu testen, ist, das Versprechen zu erfüllen, indem Sie es mit einem Wert auflösen:

const promise = new Promise((resolve, reject) => {
  resolve('We did it!')
})

Wenn Sie das Versprechen nun inspizieren, werden Sie feststellen, dass es den Status erfüllt hat und ein Wert auf den von Ihnen an resolve übergebenen Wert gesetzt ist:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: "We did it!"

Wie zu Beginn dieses Abschnitts angegeben, ist ein Versprechen ein Objekt, das einen Wert zurückgeben kann. Nach erfolgreicher Erfüllung wird der Wert von undefined auf mit Daten gefüllt geändert.

A promise can have three possible states: pending, fulfilled, and rejected.

  • Ausstehend – Ausgangszustand vor Auflösung oder Ablehnung
  • Erfüllt – Erfolgreiche Operation, Versprechen wurde erfüllt
  • Ablehnen – Fehlgeschlagene Operation, Versprechen wurde abgelehnt

Nachdem ein Versprechen erfüllt oder abgelehnt wurde, ist es abgeschlossen.

Jetzt, da Sie eine Vorstellung davon haben, wie Versprechen erstellt werden, wollen wir uns ansehen, wie ein Entwickler diese Versprechen nutzen kann.

Versprechen nutzen

Das Versprechen im letzten Abschnitt wurde mit einem Wert erfüllt, aber Sie möchten auch auf den Wert zugreifen können. Versprechen haben eine Methode namens then, die nach dem Erreichen von resolve im Code ausgeführt wird. then gibt den Wert des Versprechens als Parameter zurück.

So geben Sie den Wert des Beispielversprechens zurück und protokollieren ihn:

promise.then((response) => {
  console.log(response)
})

Das von Ihnen erstellte Versprechen hatte einen [[PromiseValue]] von Wir haben es geschafft!. Dieser Wert wird als Reaktion in die anonyme Funktion übergeben:

Output
We did it!

Bisher war das von Ihnen erstellte Beispiel nicht mit einer asynchronen Web-API verbunden – es erklärte nur, wie man ein natives JavaScript-Versprechen erstellt, erfüllt und verbraucht. Mit setTimeout können Sie eine asynchrone Anfrage testen.

Der folgende Code simuliert Daten, die von einer asynchronen Anfrage als Versprechen zurückgegeben werden:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Resolving an asynchronous request!'), 2000)
})

// Protokollieren Sie das Ergebnis
promise.then((response) => {
  console.log(response)
})

Die Verwendung der Syntax then stellt sicher, dass die response nur protokolliert wird, wenn die setTimeout-Operation nach 2000 Millisekunden abgeschlossen ist. All dies geschieht ohne Verschachtelung von Rückrufen.

Nun, nach zwei Sekunden, wird der Wert des Versprechens gelöst und im then protokolliert:

Output
Resolving an asynchronous request!

Versprechen können auch verkettet werden, um Daten an mehr als eine asynchrone Operation weiterzugeben. Wenn ein Wert in then zurückgegeben wird, kann ein weiteres then hinzugefügt werden, das mit dem Rückgabewert des vorherigen then erfüllt wird:

// Eine Promise-Kette erstellen
promise
  .then((firstResponse) => {
    // Einen neuen Wert für das nächste then zurückgeben
    return firstResponse + ' And chaining!'
  })
  .then((secondResponse) => {
    console.log(secondResponse)
  })

Die erfüllte Antwort im zweiten then wird den Rückgabewert protokollieren:

Output
Resolving an asynchronous request! And chaining!

Da then verkettet werden kann, ermöglicht es, dass die Verwendung von Versprechen synchroner erscheint als Rückrufe, da sie nicht verschachtelt werden müssen. Dies ermöglicht einen lesbareren Code, der einfacher gewartet und überprüft werden kann.

Fehlerbehandlung

Bisher haben Sie nur ein Versprechen mit einer erfolgreichen resolve-Anweisung behandelt, die das Versprechen in einen erfüllten Zustand versetzt. Häufig müssen Sie jedoch bei einer asynchronen Anfrage auch einen Fehler behandeln – wenn die API nicht verfügbar ist oder eine fehlerhafte oder nicht autorisierte Anfrage gesendet wird. Ein Versprechen sollte beide Fälle behandeln können. In diesem Abschnitt erstellen Sie eine Funktion, um sowohl den Erfolgs- als auch den Fehlerfall beim Erstellen und Verbrauchen eines Versprechens zu testen.

Diese getUsers-Funktion wird ein Flag an ein Versprechen übergeben und das Versprechen zurückgeben:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Behandeln Sie resolve und reject in der asynchronen API
    }, 1000)
  })
}

Richten Sie den Code so ein, dass wenn onSuccess true ist, der Timeout mit einigen Daten erfüllt wird. Wenn false, wird die Funktion mit einem Fehler abgelehnt:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Behandeln Sie resolve und reject in der asynchronen API
      if (onSuccess) {
        resolve([
          {id: 1, name: 'Jerry'},
          {id: 2, name: 'Elaine'},
          {id: 3, name: 'George'},
        ])
      } else {
        reject('Failed to fetch data!')
      }
    }, 1000)
  })
}

Für das erfolgreiche Ergebnis geben Sie JavaScript-Objekte zurück, die Beispieldaten für Benutzer darstellen.

Um den Fehler zu behandeln, verwenden Sie die catch-Methode. Dadurch erhalten Sie einen Fehler-Callback mit dem Fehler als Parameter.

Führen Sie den Befehl getUser mit onSuccess auf false gesetzt aus, indem Sie die then-Methode für den Erfolgsfall und die catch-Methode für den Fehler verwenden:

// Führen Sie die getUsers-Funktion mit dem falschen Flag aus, um einen Fehler auszulösen
getUsers(false)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

Seit der Fehler ausgelöst wurde, wird das then übersprungen und das catch wird den Fehler behandeln:

Output
Failed to fetch data!

Wenn Sie die Flagge umschalten und stattdessen resolve, wird das catch ignoriert und die Daten werden zurückgegeben:

// Führen Sie die getUsers-Funktion mit der true-Flagge aus, um erfolgreich aufzulösen
getUsers(true)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

Dies liefert die Benutzerdaten:

Output
(3) [{…}, {…}, {…}] 0: {id: 1, name: "Jerry"} 1: {id: 2, name: "Elaine"} 3: {id: 3, name: "George"}

Zu Referenzzwecken hier eine Tabelle mit den Handler-Methoden für Promise-Objekte:

Method Description
then() Handles a resolve. Returns a promise, and calls onFulfilled function asynchronously
catch() Handles a reject. Returns a promise, and calls onRejected function asynchronously
finally() Called when a promise is settled. Returns a promise, and calls onFinally function asynchronously

Versprechen können verwirrend sein, sowohl für neue Entwickler als auch für erfahrene Programmierer, die noch nie in einer asynchronen Umgebung gearbeitet haben. Wie bereits erwähnt, ist es jedoch viel häufiger, Versprechen zu konsumieren, als sie zu erstellen. Normalerweise wird ein Web-API des Browsers oder eine Bibliothek von Drittanbietern das Versprechen bereitstellen, und Sie müssen es nur konsumieren.

In der abschließenden Versprechen-Sektion wird dieses Tutorial einen häufigen Anwendungsfall einer Web-API, die Versprechen zurückgibt, zitieren: die Fetch-API.

Verwendung der Fetch-API mit Versprechen

Eine der nützlichsten und häufig verwendeten Web-APIs, die ein Versprechen zurückgibt, ist die Fetch-API, mit der Sie eine asynchrone Ressourcenanforderung über ein Netzwerk durchführen können. fetch ist ein zweiteiliger Prozess und erfordert daher die Verkettung von then. Dieses Beispiel zeigt, wie Sie die GitHub-API aufrufen, um die Daten eines Benutzers abzurufen, und gleichzeitig eventuelle Fehler behandeln:

// Einen Benutzer von der GitHub-API abrufen
fetch('https://api.github.com/users/octocat')
  .then((response) => {
    return response.json()
  })
  .then((data) => {
    console.log(data)
  })
  .catch((error) => {
    console.error(error)
  })

Die fetch-Anfrage wird an die URL https://api.github.com/users/octocat gesendet, die asynchron auf eine Antwort wartet. Das erste then übergibt die Antwort an eine anonyme Funktion, die die Antwort als JSON-Daten formatiert, und übergibt das JSON an ein zweites then, das die Daten in die Konsole protokolliert. Das catch-Statement protokolliert etwaige Fehler in die Konsole.

Die Ausführung dieses Codes ergibt folgendes Ergebnis:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

Dies sind die angeforderten Daten von https://api.github.com/users/octocat, dargestellt im JSON-Format.

Dieser Abschnitt des Tutorials zeigt, dass Promises viele Verbesserungen für den Umgang mit asynchronem Code bieten. Aber während die Verwendung von then zur Behandlung asynchroner Aktionen einfacher zu verfolgen ist als die Pyramide von Rückrufen, bevorzugen einige Entwickler immer noch ein synchrones Format zum Schreiben asynchronen Codes. Um diesem Bedürfnis gerecht zu werden, hat ECMAScript 2016 (ES7) die Verwendung von async-Funktionen und des await-Schlüsselworts eingeführt, um die Arbeit mit Promises zu erleichtern.

Asynchrone Funktionen mit async/await

Eine async-Funktion ermöglicht es Ihnen, asynchronen Code auf eine Art und Weise zu behandeln, die synchron erscheint. async-Funktionen verwenden immer noch Promises im Hintergrund, haben jedoch eine traditionellere JavaScript-Syntax. In diesem Abschnitt werden Sie Beispiele für diese Syntax ausprobieren.

Sie können eine async-Funktion erstellen, indem Sie das async-Schlüsselwort vor eine Funktion setzen:

// Erstellen Sie eine async-Funktion
async function getUser() {
  return {}
}

Obwohl diese Funktion noch nichts Asynchrones behandelt, verhält sie sich anders als eine traditionelle Funktion. Wenn Sie die Funktion ausführen, werden Sie feststellen, dass sie ein Promise mit einem [[PromiseStatus]] und [[PromiseValue]] anstelle eines Rückgabewerts zurückgibt.

Probieren Sie dies aus, indem Sie einen Aufruf der getUser-Funktion protokollieren:

console.log(getUser())

Dies wird Folgendes liefern:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: Object

Dies bedeutet, dass Sie eine async-Funktion mit then genauso behandeln können, wie Sie ein Promise behandeln könnten. Probieren Sie dies mit folgendem Code aus:

getUser().then((response) => console.log(response))

Dieser Aufruf von getUser übergibt den Rückgabewert an eine anonyme Funktion, die den Wert in die Konsole protokolliert.

Sie erhalten Folgendes, wenn Sie dieses Programm ausführen:

Output
{}

Eine async-Funktion kann ein innerhalb von ihr aufgerufenes Versprechen mit dem await-Operator verarbeiten. await kann innerhalb einer async-Funktion verwendet werden und wartet, bis sich ein Versprechen erfüllt hat, bevor der bestimmte Code ausgeführt wird.

Mit diesem Wissen können Sie die Fetch-Anforderung aus dem letzten Abschnitt mit async/await wie folgt neu schreiben:

// Fetch mit async/await behandeln
async function getUser() {
  const response = await fetch('https://api.github.com/users/octocat')
  const data = await response.json()

  console.log(data)
}

// Asynchrone Funktion ausführen
getUser()

Die await-Operatoren hier stellen sicher, dass die data nicht protokolliert wird, bevor die Anforderung sie mit Daten gefüllt hat.

Jetzt kann die endgültige data innerhalb der getUser-Funktion behandelt werden, ohne dass then verwendet werden muss. Dies ist die Ausgabe des Protokollierens von data:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

Hinweis: In vielen Umgebungen ist async erforderlich, um await zu verwenden. Einige neue Versionen von Browsern und Node erlauben jedoch die Verwendung von Top-Level-await, was es Ihnen ermöglicht, das Erstellen einer async-Funktion zu umgehen, um das await einzuschließen.

Zuletzt, da Sie das erfüllte Versprechen innerhalb der asynchronen Funktion verarbeiten, können Sie auch den Fehler innerhalb der Funktion behandeln. Anstelle der Verwendung der catch-Methode mit then verwenden Sie das try/catch-Muster, um die Ausnahme zu behandeln.

Fügen Sie den folgenden markierten Code hinzu:

// Erfolg und Fehlerbehandlung mit async/await
async function getUser() {
  try {
    // Erfolg in try behandeln
    const response = await fetch('https://api.github.com/users/octocat')
    const data = await response.json()

    console.log(data)
  } catch (error) {
    // Fehler in catch behandeln
    console.error(error)
  }
}

Das Programm wird nun zum catch-Block springen, wenn es einen Fehler erhält, und diesen Fehler in die Konsole protokollieren.

Moderne asynchrone JavaScript-Code wird meistens mit der Syntax async/await behandelt, aber es ist wichtig, ein Grundwissen darüber zu haben, wie Promises funktionieren, insbesondere da Promises zusätzliche Funktionen bieten können, die nicht mit async/await behandelt werden können, wie das Kombinieren von Promises mit Promise.all().

Hinweis: async/await kann durch die Verwendung von Generatoren in Kombination mit Promises reproduziert werden, um Ihrem Code mehr Flexibilität zu verleihen. Um mehr zu erfahren, schauen Sie sich unser Tutorial Understanding Generators in JavaScript an.

Fazit

Weil Web-APIs oft Daten asynchron bereitstellen, ist das Erlernen des Umgangs mit dem Ergebnis asynchroner Aktionen ein wesentlicher Bestandteil der Arbeit als JavaScript-Entwickler. In diesem Artikel haben Sie gelernt, wie die Host-Umgebung die Ereignisschleife verwendet, um die Reihenfolge der Codeausführung mit dem Stack und der Warteschlange zu verwalten. Sie haben auch Beispiele für drei Möglichkeiten ausprobiert, den Erfolg oder das Scheitern eines asynchronen Ereignisses zu behandeln, mit Callbacks, Promises und der async/await-Syntax. Schließlich haben Sie die Fetch Web-API verwendet, um asynchrone Aktionen zu behandeln.

Für weitere Informationen darüber, wie der Browser parallele Ereignisse behandelt, lesen Sie Concurrency-Modell und die Ereignisschleife auf dem Mozilla Developer Network. Wenn Sie mehr über JavaScript erfahren möchten, kehren Sie zu unserer How To Code in JavaScript-Serie zurück.

Source:
https://www.digitalocean.com/community/tutorials/understanding-the-event-loop-callbacks-promises-and-async-await-in-javascript