mitmario.dev

Was asynchron heißt

JavaScript Im Browser 5 Min Lesezeit 3 BeispieleLektion 2 von 8

Diese Lektion bringt kein neues Sprachmittel. Sie bringt ein Bild, und ohne dieses Bild ist alles Weitere auswendig gelernt.

Wer es einmal gesehen hat, braucht bei await keine Regel mehr, weiß, warum ein try/catch um einen Timer nichts fängt, und versteht, warum eine Seite manchmal einfriert.

Eine Sache zur Zeit

JavaScript hat genau einen Aufrufstapel. Er arbeitet eine Anweisung nach der anderen ab, und solange er beschäftigt ist, passiert sonst nichts. Kein Klick, kein Timer, kein Neuzeichnen.

Das ist erst einmal eine Einschränkung, hat aber einen großen Vorteil: Du musst dir nie Gedanken darüber machen, dass zwei Teile deines Codes gleichzeitig an derselben Variablen herumschreiben. Wenn deine Funktion läuft, läuft sonst nichts.

Die Frage ist dann natürlich, wie eine Seite trotzdem etwas laden kann, während der Benutzer weiterklickt.

Der Browser macht die Arbeit woanders

Die Antwort ist, dass der Stapel diese Arbeit gar nicht macht.

Wenn du setTimeout aufrufst, notiert sich der Browser den Auftrag. Er hat dafür eigene Uhren und eigene Verbindungen, und die laufen außerhalb des Stapels. Der Aufruf selbst ist sofort fertig, und dein Code läuft weiter.

Ist die Zeit um, legt der Browser die Funktion in eine Warteschlange. Er ruft sie nicht auf. Er stellt sie an.

Und dann wartet er, bis der Stapel leer ist. Erst dann nimmt er den ersten Eintrag aus der Warteschlange und legt ihn auf den Stapel.

Dieser Kreislauf hat einen Namen: die Event Loop. Er ist die Antwort auf fast jede Frage der Form „warum passiert das erst danach”.

Die Reihenfolge, die alle überrascht

Die Reihenfolge, die alle überrascht
console.log("1");

setTimeout(() => {
  console.log("2");
}, 0);

Promise.resolve().then(() => {
  console.log("3");
});

setTimeout(() => {
  console.log("4");
}, 0);

console.log("5");

Jetzt lässt sich das erste Beispiel erklären. In der Console steht:

1, 5, 3, 2, 4.

Der Reihe nach:

  • 1 und 5 stehen direkt im Skript. Sie laufen sofort, denn sie liegen schon auf dem Stapel.
  • 3 kommt aus einem Promise. Promises haben eine eigene Warteschlange, die Mikrotask-Warteschlange, und die hat Vorrang. Sie wird abgearbeitet, sobald der Stapel leer ist, und zwar vollständig.
  • 2 und 4 kommen aus Timern. Timer landen in der Makrotask-Warteschlange, und die ist erst danach dran. Untereinander behalten sie ihre Reihenfolge: 2 wurde vor 4 angemeldet, also läuft 2 vor 4.

Beide setTimeout stehen auf null Millisekunden, und trotzdem laufen sie zuletzt. Das ist keine Ungenauigkeit der Uhr. Es ist die Regel: Alles Wartende kommt erst dran, wenn der Stapel leer ist.

Zwei Warteschlangen nebeneinander
console.log("Skript beginnt");

setTimeout(() => console.log("Timer A"), 0);
Promise.resolve().then(() => console.log("Mikrotask A"));
setTimeout(() => console.log("Timer B"), 0);
Promise.resolve().then(() => console.log("Mikrotask B"));
setTimeout(() => console.log("Timer C"), 0);
Promise.resolve().then(() => console.log("Mikrotask C"));

console.log("Skript endet");

Im dritten Beispiel siehst du dasselbe noch einmal ohne Ablenkung. Angemeldet wird abwechselnd, aber in der Console kommen erst alle drei Mikrotasks und danach alle drei Timer.

Den Stapel und die Warteschlangen ansehen

Bis hierhin war das eine Erzählung. Im Reiter Debug kannst du sie nachprüfen, und nirgends im Kurs lohnt sich das so wie hier.

Hol dir das erste Beispiel zurück in den Editor und drück auf Aufzeichnen. Es stehen dann acht Schritte da, und zwar in dieser Reihenfolge: Zeile 1, 3, 7, 11, 15, und danach 8, 4, 12.

Die ersten fünf sind der Stapel. Sauber von oben nach unten durch die Datei, ohne einen einzigen Sprung. In dieser Phase ist nichts passiert außer Anmelden: Die Zeilen 3, 7 und 11 haben je einen Auftrag abgegeben und waren im selben Moment wieder fertig.

Die letzten drei sind die Warteschlangen. Jetzt springt die Liste zurück nach oben, denn dort stehen die Rümpfe. Zuerst Zeile 8, das ist die Mikrotask. Dann Zeile 4 und Zeile 12, das sind die beiden Timer, in der Reihenfolge, in der sie angemeldet wurden.

Die Console zeigt dir die fünf Ausgaben. Der Reiter zeigt dir, warum sie in dieser Reihenfolge stehen, und das ist ein Unterschied: Aus fünf Ausgaben kannst du dir die Regel zusammenreimen, aus dem Sprung von Zeile 15 zurück auf Zeile 8 liest du sie ab.

Deshalb friert eine Seite ein

Was passiert, wenn der Stapel besetzt ist
let klicks = 0;

document.getElementById("knopf").addEventListener("click", () => {
  klicks = klicks + 1;
  document.getElementById("klicks").textContent = `Klicks: ${klicks}`;
});

// Nach zwei Sekunden blockiert diese Rechnung den Stapel.
// Klick währenddessen mehrmals auf den Knopf: Es passiert
// nichts, und danach kommt alles auf einmal.
setTimeout(() => {
  const start = Date.now();
  let summe = 0;
  for (let i = 0; i < 200000000; i++) {
    summe = summe + i;
  }
  document.getElementById("rechnung").textContent = `Gerechnet in ${Date.now() - start} ms`;
}, 2000);

Wenn der Stapel eine Sache zur Zeit abarbeitet, dann heißt eine lange Rechnung: Für diese Zeit ist alles andere blockiert.

Im zweiten Beispiel läuft nach zwei Sekunden eine Schleife los, die eine Weile rechnet. Klick währenddessen auf den Knopf. Es passiert nichts. Der Klick ist nicht verloren, er steht in der Warteschlange, aber der Stapel ist besetzt. Sobald die Rechnung fertig ist, kommen alle Klicks auf einmal an.

Das ist genau das Gefühl, das man von einer hängenden Seite kennt. Und die Ursache ist fast nie „der Rechner ist zu langsam”, sondern „irgendwo läuft eine Schleife zu lange”.

Der Ausweg für so etwas ist nicht Teil dieses Kurses. Es gibt ihn (Web Workers), und er lohnt sich erst, wenn man wirklich rechnen muss. Für alles, was mit Warten zu tun hat, ist der Ausweg genau das, was hier gerade beschrieben wurde: Nicht warten, sondern anmelden.

Was das für dich heißt

Drei Sätze zum Mitnehmen, und mit diesen dreien kommst du durch den Rest des Abschnitts:

  1. Alles, was dauert, wird angemeldet statt abgewartet. Der Code läuft sofort weiter.
  2. Das Ergebnis kommt später, in einer Funktion, die du vorher hinterlegt hast. Nur dort ist es zu haben, und nicht in der Zeile darunter.
  3. Der try-Block ist längst vorbei, wenn diese Funktion läuft. Deshalb fängt ein try/catch um einen Timer nichts. Das ist keine Macke, sondern eine direkte Folge aus Punkt 1.

Der zweite Punkt ist der, der am Anfang am meisten weh tut. Man möchte so gerne schreiben:

Ein Wert wird geladen, in der nächsten Zeile steht er in einer Variablen, in der übernächsten wird er benutzt. Genau das geht nicht. Was geht, ist eine Funktion zu hinterlegen, die aufgerufen wird, wenn der Wert da ist.

Wie diese Funktion aussieht, ist die Geschichte der nächsten drei Lektionen: erst als Callback, dann als Promise, und zum Schluss mit async und await so geschrieben, dass es wieder aussieht wie die Zeile darunter.

Zum Mitnehmen

Der Stapel arbeitet eine Sache zur Zeit ab. Alles Wartende kommt erst dran, wenn er leer ist. Zuerst die Mikrotasks, dann die Timer.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.