Skip to content

JavaScript Asynchrone Programmierung und Callbacks

Asynchronität in Programmiersprachen

Computer sind von Natur aus asynchron.

Asynchron bedeutet, dass Dinge unabhängig vom Hauptprogrammablauf geschehen können.

In den aktuellen Verbrauchercomputern läuft jedes Programm für einen bestimmten Zeitraum und stoppt dann seine Ausführung, um ein anderes Programm fortsetzen zu lassen. Dies geschieht so schnell in einem Zyklus, dass es unmöglich zu bemerken ist. Wir denken, unsere Computer führen viele Programme gleichzeitig aus, aber das ist eine Illusion (außer bei Multiprozessormaschinen).

Programme verwenden intern Interrupts, ein Signal, das an den Prozessor gesendet wird, um die Aufmerksamkeit des Systems zu erlangen.

Gehen wir jetzt nicht auf die Interna ein, sondern behalten wir einfach im Hinterkopf, dass es normal ist, dass Programme asynchron sind und ihre Ausführung anhalten, bis sie Aufmerksamkeit benötigen, wodurch der Computer in der Zwischenzeit andere Dinge ausführen kann. Wenn ein Programm auf eine Antwort vom Netzwerk wartet, kann es den Prozessor nicht anhalten, bis die Anfrage abgeschlossen ist.

Normalerweise sind Programmiersprachen synchron und einige bieten eine Möglichkeit, Asynchronität in der Sprache oder über Bibliotheken zu verwalten. C, Java, C#, PHP, Go, Ruby, Swift und Python sind standardmäßig synchron. Einige von ihnen behandeln asynchrone Operationen mithilfe von Threads, indem sie einen neuen Prozess erzeugen.

JavaScript

JavaScript ist standardmäßig synchron und Single-Threaded. Das bedeutet, dass Code keine neuen Threads erstellen und parallel ausführen kann.

Codezeilen werden nacheinander in Reihe ausgeführt, zum Beispiel:

js
const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();

Aber JavaScript wurde im Browser geboren. Seine Hauptaufgabe war es anfangs, auf Benutzeraktionen wie onClick, onMouseOver, onChange, onSubmit usw. zu reagieren. Wie konnte es dies mit einem synchronen Programmiermodell tun?

Die Antwort lag in seiner Umgebung. Der Browser bietet eine Möglichkeit, dies zu tun, indem er eine Reihe von APIs bereitstellt, die diese Art von Funktionalität verarbeiten können.

In jüngerer Zeit hat Node.js eine nicht-blockierende I/O-Umgebung eingeführt, um dieses Konzept auf Dateizugriffe, Netzwerkaufrufe usw. auszudehnen.

Callbacks

Du kannst nicht wissen, wann ein Benutzer auf eine Schaltfläche klickt. Daher definierst du einen Event-Handler für das Klickereignis. Dieser Event-Handler akzeptiert eine Funktion, die aufgerufen wird, wenn das Ereignis ausgelöst wird:

js
document.getElementById('button').addEventListener('click', () => {
  // Element wurde geklickt
});

Dies ist der sogenannte Callback.

Ein Callback ist eine einfache Funktion, die als Wert an eine andere Funktion übergeben wird und nur ausgeführt wird, wenn das Ereignis eintritt. Wir können dies tun, weil JavaScript Funktionen erster Klasse hat, die Variablen zugewiesen und an andere Funktionen übergeben werden können (sogenannte Funktionen höherer Ordnung)

Es ist üblich, den gesamten Client-Code in einen load-Event-Listener auf dem window-Objekt einzuschließen, der die Callback-Funktion nur dann ausführt, wenn die Seite bereit ist:

js
window.addEventListener('load', () => {
  // Fenster geladen
  // Tue, was du willst
});

Callbacks werden überall verwendet, nicht nur bei DOM-Ereignissen.

Ein häufiges Beispiel ist die Verwendung von Timern:

js
setTimeout(() => {
  // wird nach 2 Sekunden ausgeführt
}, 2000);

XHR-Anfragen akzeptieren ebenfalls einen Callback, in diesem Beispiel durch Zuweisen einer Funktion zu einer Eigenschaft, die aufgerufen wird, wenn ein bestimmtes Ereignis eintritt (in diesem Fall ändert sich der Status der Anfrage):

js
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
  if (xhr.readyState === 4) {
    xhr.status === 200 ? console.log(xhr.responseText) : console.error('Fehler');
  }
};
xhr.open('GET', 'https://yoursite.com');
xhr.send();

Fehlerbehandlung in Callbacks

Wie werden Fehler mit Callbacks behandelt? Eine sehr gängige Strategie ist die Verwendung dessen, was Node.js übernommen hat: Der erste Parameter in jeder Callback-Funktion ist das Fehlerobjekt: Error-First-Callbacks

Wenn kein Fehler vorliegt, ist das Objekt null. Wenn ein Fehler vorliegt, enthält es eine Beschreibung des Fehlers und andere Informationen.

js
const fs = require('node:fs');
fs.readFile('/file.json', (err, data) => {
  if (err) {
    // Fehler behandeln
    console.log(err);
    return;
  }
  // keine Fehler, Daten verarbeiten
  console.log(data);
});

Das Problem mit Callbacks

Callbacks sind großartig für einfache Fälle!

Allerdings fügt jeder Callback eine Ebene der Verschachtelung hinzu, und wenn man viele Callbacks hat, wird der Code sehr schnell kompliziert:

js
window.addEventListener('load', () => {
  document.getElementById('button').addEventListener('click', () => {
    setTimeout(() => {
      items.forEach(item => {
        // Dein Code hier
      });
    }, 2000);
  });
});

Dies ist nur ein einfacher 4-Ebenen-Code, aber ich habe schon viel mehr Ebenen der Verschachtelung gesehen, und das macht keinen Spaß.

Wie lösen wir das?

Alternativen zu Callbacks

Beginnend mit ES6 hat JavaScript mehrere Features eingeführt, die uns bei asynchronem Code helfen, ohne Callbacks zu verwenden: Promises (ES6) und Async/Await (ES2017).