Ein Benutzer klickt Datei A, dann B. Der Titel zeigt B, der Editor aber den Inhalt von A. Beide Lesevorgänge lieferten richtige Daten. Das Problem: As Ergebnis kam, nachdem der Benutzer weitergegangen war, und nichts verhinderte seine Anzeige.
Eine Aufrufkette aus Klick, Laden und Rendern erklärt den Fehler nicht, denn beide Klicks durchlaufen dieselben Funktionen. Entscheidend sind Beginn und Ende jeder Anfrage sowie ihr Recht, die Ansicht zu ändern. Dieser Beitrag zeigt ein kleines JavaScript-Beispiel einschließlich Fehler- und Ladezuständen, die schnelle Korrekturen oft übersehen.
Bis zum ersten await lesen
Hier ist ein vereinfachter Dateileser. load liefert ein Promise für den Text, publish fügt einen Patch in den sichtbaren Zustand ein. Beide werden übergeben; das Beispiel hängt von keinem UI-Framework ab:
export function createUnsafeReader(load, publish) {
return async function select(file) {
publish({ file, text: '', error: null, loading: true });
try {
const text = await load(file);
publish({ text });
} catch (error) {
publish({ error: error.message });
} finally {
publish({ loading: false });
}
};
}
Vor await speichert die Funktion die Auswahl und löscht alten Text. Während des Wartens kann ein weiterer select-Aufruf beginnen. Nach await wird nicht geprüft, ob die Anfrage noch zur sichtbaren Auswahl gehört.
Die lokale Variable file bleibt während der Unterbrechung gleich, die gemeinsame Ansicht aber nicht. Das ist der gesamte Fehler: Die Daten passen noch zum eigenen Aufruf, doch dieser ist nicht mehr der anzuzeigende.
Eine Zeitachse zeichnen
Nutzen Sie zwei steuerbare Lesevorgänge und lassen Sie B vor A fertig werden:
| Schritt | Ereignis | Gewählte Datei | Sichtbarer Text | Lädt |
|---|---|---|---|---|
| 1 | A wählen; Lesen beginnt | A | leer | ja |
| 2 | B wählen; Lesen beginnt | B | leer | ja |
| 3 | B erfolgreich | B | Text von B | nein |
| 4 | A erfolgreich | B | Text von A | nein |
Beide Aufrufe liefen durch denselben Code. Wegen ihrer Reihenfolge gewann das zuletzt fertige Lesen, obwohl die zuletzt gewählte Datei gewinnen sollte.
Klären Sie vor der Korrektur das gewünschte UI-Verhalten. Eine einzelne Vorschau zeigt die neueste Auswahl. Ein Stapelimport braucht alle Ergebnisse, eine Speicherwarteschlange eventuell geordnete Schreibvorgänge; alte Ergebnisse zu verwerfen wäre für beide falsch. Die folgende Korrektur gilt für Ansichten, in denen neue Auswahl alte ersetzt.
Ohne Timer reproduzieren
Ein Test mit 100 ms für A und 10 ms für B ist langsam und hängt vom Scheduler ab. Ein Deferred Promise lässt den Test den Abschluss exakt bestimmen:
function deferred() {
let resolve, reject;
const promise = new Promise((yes, no) => {
resolve = yes;
reject = no;
});
return { promise, resolve, reject };
}
Erstellen Sie zwei verzögerte Lesevorgänge, wählen Sie beide Dateien, lösen Sie B und danach A auf:
const a = deferred(),
b = deferred();
const state = {};
const select = createUnsafeReader(
(file) => (file === 'A' ? a : b).promise,
(patch) => Object.assign(state, patch),
);
const first = select('A'),
second = select('B');
b.resolve('B text');
await second;
a.resolve('A text');
await first;
assert.equal(state.file, 'B');
assert.equal(state.text, 'A text'); // Reproduces the defect.
Die letzte Assertion verlangt absichtlich das falsche Ergebnis und zeigt damit den Fehler. Der Test des korrigierten Lesers erwartet stattdessen Bs Text. Getrennte Reproduktions- und Korrekturtests verhindern, dass eine grüne Reproduktion als Beleg für funktionierenden Altcode missverstanden wird.
Jede Auswahl nummerieren
Für eine einzelne Ansicht genügt ein Zähler. Erhöhen Sie ihn beim Start, behalten Sie den Wert im Aufruf und vergleichen Sie ihn vor jeder Veröffentlichung:
export function createReader(load, publish) {
let latest = 0;
return async function select(file) {
const request = ++latest;
publish({ file, text: '', error: null, loading: true });
try {
const text = await load(file);
if (request === latest) publish({ text });
} catch (error) {
if (request === latest) publish({ error: error.message });
} finally {
if (request === latest) publish({ loading: false });
}
};
}
Der Zähler gehört dieser Leserinstanz. Global würde ein Klick in einem Fenster Ergebnisse eines anderen verwerfen. Innerhalb von select würde er bei jedem Aufruf zurückgesetzt und könnte Anfragen nicht unterscheiden.
Auch Dateipfade zu vergleichen reicht nicht. Bei A, dann B, dann wieder A haben erste und dritte Anfrage denselben Pfad, doch die erste kann eine ältere Dateiversion enthalten. Die Anfragenummer unterscheidet sie.
Das Beispiel setzt synchrone Zustandsänderung durch publish und Ablehnung von load mit einem Error voraus. Bündelt Ihr Framework Updates, entfernt Komponenten oder lehnt die API mit anderen Werten ab, müssen Sie auch das behandeln.
A → B → A mit drei eigenen Lesevorgängen testen
Geben Sie jedem Aufruf ein eigenes Deferred Promise. Die frühere Fixture ordnet A und B je einem Promise zu; Wiederverwendung für ein zweites A würde den Unterschied zwischen erster und dritter Anfrage verbergen.
Nehmen Sie an, load kann für denselben Pfad verschiedene Stände liefern, etwa bei Änderungen zwischen Lesevorgängen. Der Test liefert ausdrücklich „A version 1“ und „A version 2“. Das sind kontrollierte Lehrbeispiele, kein Yenpo-Produktionsvorfall. Liefert Ihr Loader immer denselben unveränderlichen Stand, entsteht hier kein Textunterschied. Prüfen Sie tatsächliches Caching und Lesen, bevor Sie diese Erklärung übernehmen.
Der Download enthält einen absichtlich unvollständigen createPathOnlyReader. Er verfolgt selectedFile und schützt Erfolg, Fehler und finally mit file === selectedFile. Sobald A erneut gewählt ist, akzeptieren alle drei Prüfungen auch das erste A:
| Ereignis | Nur Pfadprüfung | Anfragenummer-Prüfung |
|---|---|---|
| A, B, dann wieder A wählen | Aktueller Pfad A | Aktuelle Anfrage 3 |
| Dritte Anfrage liefert A Version 2 | Zeigt A Version 2 | Zeigt A Version 2 |
| B kommt zurück | Ignoriert B | Ignoriert Anfrage 2 |
| Erste Anfrage liefert A Version 1 | Zeigt A Version 1 | Behält A Version 2 |
Die gepaarten Tests führen genau diese Reihenfolge mit beiden Lesern aus. Der Test reproduce: erwartet die falsche Version und belegt das Scheitern der Pfadprüfung; der Nummerntest erwartet Version 2. Zwei weitere Tests beenden das erste A, während das dritte noch wartet: Alter Erfolg muss Text leer und Laden aktiv lassen, alter Fehler darf keinen Fehler setzen. Der Fehler der aktuellen A-Anfrage muss weiterhin erscheinen.
Zwei Leser unabhängig halten
Erstellen Sie pro unabhängiger Ansicht einen Leser mit eigenem latest-Closure und eigenem publish-Ziel. Verwenden Sie ihn bei späteren Auswahlen derselben Ansicht weiter. Neuerzeugung bei jedem Klick verliert den gemeinsamen Zähler, der alte Aufrufe ungültig macht.
Der letzte Test erstellt linke und rechte Leser mit getrennten Deferred-Loadern und Zustandsobjekten und führt Folgendes aus:
- A links, A rechts, dann B links wählen. Links ist die aktuelle Nummer 2, rechts weiterhin 1.
- Rechtes A abschließen, während linkes B wartet. Rechts muss das Ergebnis zeigen und Laden beenden; links bleibt leer und lädt.
- Das alte linke A ablehnen. Links muss ohne Fehler weiter auf B warten.
- Linkes B abschließen. Links zeigt B, rechts weiterhin sein eigenes A.
Ein latest auf Modulebene lässt Schritt 2 scheitern: Die neuere linke Auswahl invalidiert die rechte Anfrage und lässt sie laden. Ein gemeinsames sichtbares Zustandsobjekt würde die Isolation auch bei getrennten Zählern verletzen. Der Test prüft beide Ansichten nach den verschachtelten Abschlüssen.
Auch Fehler und Ladezustand können falsch landen
Wenn Sie nur publish({ text }) schützen, verschwindet falscher Text, aber zwei Fehler bleiben:
- A scheitert nach der Auswahl von B. As
catchzeigt „A failed“ in Bs Ansicht. - A endet, während B noch lädt. As
finallyschaltet Bs Ladeanzeige ab.
Beenden Sie zum Erkennen des zweiten Fehlers A, solange B noch wartet, und prüfen Sie sofort, ob loading weiter true ist. Erst nach Bs Ende zu prüfen würde das vorzeitige Abschalten verdecken.
Für den ersten Fehler lehnen Sie A ab, während B wartet, und prüfen Sie Fehlerfreiheit und aktives Laden bei B. Lösen Sie dann B auf. Ergänzen Sie einen Test für den Fehler der aktuellen Anfrage: Eine Prüfung, die alle Fehler verschluckt, würde den alten Fehlerfall bestehen und echte Fehler verstecken.
Die Regel ist also strenger als „irgendwann erscheint der richtige Text“: Nur die aktuelle Anfrage darf beim Abschluss etwas veröffentlichen – Text, Fehler oder das Abschalten der Ladeanzeige.
Abbrechen ist nicht Ignorieren
Unterstützt Ihr Loader Abbruch, spart das Abbrechen von A Arbeit. A kann dennoch danach veröffentlichen: Das Ergebnis ist vielleicht schon gecacht, die Operation ignoriert das Signal oder endet gleichzeitig mit dem Abbruch.
Nutzen Sie Abbruch für nicht mehr benötigte Arbeit. Nutzen Sie die Anfragenummer für die Entscheidung, ob ein Ergebnis die Ansicht ändern darf. Prüfen Sie, ob der Loader ein AbortSignal wirklich beachtet; nicht jede Promise-API tut das.
Ein altes Ergebnis zu ignorieren macht auch nichts rückgängig, was load unterwegs getan hat. Hier liefert der Loader nur Text. Schreibt ein echter Loader zusätzlich in einen gemeinsamen Cache oder ein Modell, verfolgen Sie dies separat. Der Zähler schützt nur die Ansicht.
Tests ausführen
Laden Sie async-selection.mjs herunter und führen Sie es mit Node.js 22 oder neuer aus:
node --test async-selection.mjs
Die zehn Tests prüfen ursprüngliches Überschreiben bei A → B, Erfolg/Fehler/Laden, wiederholtes A → B → A und zwei unabhängige Leser. Die zwei Tests reproduce: erwarten absichtlich falsche Ergebnisse; acht weitere prüfen das beabsichtigte Verhalten. Ohne Timer und Abhängigkeiten.
Finden Sie für echte Projekte Einstiegshandler, jedes await oder Callback, während des Wartens veränderlichen gemeinsamen Zustand und jeden Schreibzugriff auf dem Rückweg. Notieren Sie die Ereignisfolge neben den Quellstellen.
In Yenpo können Sie die Einstiegsdatei geöffnet halten und den AI Reader bitten, Zustand über ein await hinweg zu verfolgen. Prüfen Sie Erfolg, Fehler und finally gegen seine Antwort. Endet sie bei „Das Promise wird aufgelöst“, fragen Sie nach: Welche Anfrage darf das Ergebnis verwenden?