Meistern Sie JavaScript Promise Combinators (Promise.all, Promise.allSettled, Promise.race, Promise.any) für eine effiziente und robuste asynchrone Programmierung in globalen Anwendungen.
JavaScript Promise Combinators: Fortgeschrittene asynchrone Muster für globale Anwendungen
Asynchrone Programmierung ist ein Eckpfeiler des modernen JavaScript, insbesondere bei der Erstellung von Webanwendungen, die mit APIs und Datenbanken interagieren oder zeitaufwändige Operationen durchführen. JavaScript Promises bieten eine leistungsstarke Abstraktion zur Verwaltung asynchroner Operationen, aber um sie zu meistern, ist das Verständnis fortgeschrittener Muster erforderlich. Dieser Artikel befasst sich mit JavaScript Promise Combinators – Promise.all, Promise.allSettled, Promise.race und Promise.any – und wie sie verwendet werden können, um effiziente und robuste asynchrone Arbeitsabläufe zu erstellen, insbesondere im Kontext globaler Anwendungen mit unterschiedlichen Netzwerkbedingungen und Datenquellen.
Promises verstehen: Eine kurze Zusammenfassung
Bevor wir uns mit Combinators befassen, lassen Sie uns die Promises kurz wiederholen. Ein Promise repräsentiert das schließliche Ergebnis einer asynchronen Operation. Es kann sich in einem von drei Zuständen befinden:
- Pending: Der Anfangszustand; weder erfüllt noch abgelehnt.
- Fulfilled: Die Operation wurde erfolgreich abgeschlossen, mit einem Ergebniswert.
- Rejected: Die Operation ist fehlgeschlagen, mit einem Grund (normalerweise ein Error-Objekt).
Promises bieten im Vergleich zu herkömmlichen Callbacks eine sauberere und besser handhabbare Möglichkeit, asynchrone Operationen zu behandeln. Sie verbessern die Lesbarkeit des Codes und vereinfachen die Fehlerbehandlung. Entscheidend ist, dass sie auch die Grundlage für die Promise Combinators bilden, die wir uns ansehen werden.
Promise Combinators: Asynchrone Operationen orchestrieren
Promise Combinators sind statische Methoden auf dem Promise-Objekt, die es Ihnen ermöglichen, mehrere Promises zu verwalten und zu koordinieren. Sie bieten leistungsstarke Werkzeuge zum Erstellen komplexer asynchroner Arbeitsabläufe. Schauen wir uns jeden einzelnen im Detail an.
Promise.all(): Paralleles Ausführen von Promises und Zusammenfassen der Ergebnisse
Promise.all() nimmt ein iterierbares Objekt (normalerweise ein Array) von Promises als Eingabe und gibt ein einzelnes Promise zurück. Dieses zurückgegebene Promise wird erfüllt, wenn alle Eingabe-Promises erfüllt wurden. Wenn eines der Eingabe-Promises abgelehnt wird, wird das zurückgegebene Promise sofort mit dem Grund des ersten abgelehnten Promises abgelehnt.
Anwendungsfall: Wenn Sie Daten von mehreren APIs gleichzeitig abrufen und die kombinierten Ergebnisse verarbeiten müssen, ist Promise.all() ideal. Stellen Sie sich zum Beispiel vor, Sie erstellen ein Dashboard, das Wetterinformationen aus verschiedenen Städten der Welt anzeigt. Die Daten jeder Stadt könnten über einen separaten API-Aufruf abgerufen werden.
async function fetchWeatherData(city) {
try {
const response = await fetch(`https://api.example.com/weather?city=${city}`); // Durch einen echten API-Endpunkt ersetzen
if (!response.ok) {
throw new Error(`Wetterdaten für ${city} konnten nicht abgerufen werden`);
}
return await response.json();
} catch (error) {
console.error(`Fehler beim Abrufen der Wetterdaten für ${city}: ${error}`);
throw error; // Fehler erneut werfen, damit er von Promise.all abgefangen wird
}
}
async function displayWeatherData() {
const cities = ['London', 'Tokio', 'New York', 'Sydney'];
try {
const weatherDataPromises = cities.map(city => fetchWeatherData(city));
const weatherData = await Promise.all(weatherDataPromises);
weatherData.forEach((data, index) => {
console.log(`Wetter in ${cities[index]}:`, data);
// Die Benutzeroberfläche mit den Wetterdaten aktualisieren
});
} catch (error) {
console.error('Wetterdaten für alle Städte konnten nicht abgerufen werden:', error);
// Dem Benutzer eine Fehlermeldung anzeigen
}
}
displayWeatherData();
Überlegungen für globale Anwendungen:
- Netzwerklatenz: Anfragen an verschiedene APIs an unterschiedlichen geografischen Standorten können unterschiedliche Latenzen aufweisen.
Promise.all()garantiert nicht die Reihenfolge, in der die Promises erfüllt werden, sondern nur, dass alle erfüllt werden (oder eines abgelehnt wird), bevor das kombinierte Promise abgeschlossen ist. - API-Ratenbegrenzung: Wenn Sie mehrere Anfragen an dieselbe API oder an mehrere APIs mit gemeinsamen Ratenbegrenzungen stellen, könnten Sie diese Limits überschreiten. Implementieren Sie Strategien wie das Einreihen von Anfragen in eine Warteschlange oder die Verwendung von exponentiellem Backoff, um Ratenbegrenzungen elegant zu handhaben.
- Fehlerbehandlung: Denken Sie daran, dass die gesamte
Promise.all()-Operation fehlschlägt, wenn irgendein Promise abgelehnt wird. Dies ist möglicherweise nicht wünschenswert, wenn Sie Teildaten anzeigen möchten, auch wenn einige Anfragen fehlschlagen. Erwägen Sie in solchen Fällen die Verwendung vonPromise.allSettled()(wird unten erklärt).
Promise.allSettled(): Erfolg und Misserfolg individuell behandeln
Promise.allSettled() ähnelt Promise.all(), jedoch mit einem entscheidenden Unterschied: Es wartet, bis alle Eingabe-Promises abgeschlossen sind, unabhängig davon, ob sie erfüllt oder abgelehnt werden. Das zurückgegebene Promise wird immer mit einem Array von Objekten erfüllt, von denen jedes das Ergebnis des entsprechenden Eingabe-Promises beschreibt. Jedes Objekt hat eine status-Eigenschaft (entweder "fulfilled" oder "rejected") und eine value- (falls erfüllt) oder reason-Eigenschaft (falls abgelehnt).
Anwendungsfall: Wenn Sie Ergebnisse aus mehreren asynchronen Operationen sammeln müssen und es akzeptabel ist, dass einige fehlschlagen, ohne dass die gesamte Operation fehlschlägt, ist Promise.allSettled() die bessere Wahl. Stellen Sie sich ein System vor, das Zahlungen über mehrere Zahlungs-Gateways abwickelt. Sie möchten vielleicht alle Zahlungen versuchen und aufzeichnen, welche erfolgreich waren und welche fehlgeschlagen sind.
async function processPayment(paymentGateway, amount) {
try {
const response = await paymentGateway.process(amount); // Durch eine echte Zahlungs-Gateway-Integration ersetzen
if (response.status === 'success') {
return { status: 'fulfilled', value: `Zahlung über ${paymentGateway.name} erfolgreich verarbeitet` };
} else {
throw new Error(`Zahlung über ${paymentGateway.name} fehlgeschlagen: ${response.message}`);
}
} catch (error) {
return { status: 'rejected', reason: `Zahlung über ${paymentGateway.name} fehlgeschlagen: ${error.message}` };
}
}
async function processMultiplePayments(paymentGateways, amount) {
const paymentPromises = paymentGateways.map(gateway => processPayment(gateway, amount));
const results = await Promise.allSettled(paymentPromises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.error(result.reason);
}
});
// Analysieren Sie die Ergebnisse, um den Gesamterfolg/-misserfolg zu bestimmen
const successfulPayments = results.filter(result => result.status === 'fulfilled').length;
const failedPayments = results.filter(result => result.status === 'rejected').length;
console.log(`Erfolgreiche Zahlungen: ${successfulPayments}`);
console.log(`Fehlgeschlagene Zahlungen: ${failedPayments}`);
}
// Beispiel-Zahlungs-Gateways
const paymentGateways = [
{ name: 'PayPal', process: (amount) => Promise.resolve({ status: 'success', message: 'Zahlung erfolgreich' }) },
{ name: 'Stripe', process: (amount) => Promise.reject({ status: 'error', message: 'Unzureichende Deckung' }) },
{ name: 'Worldpay', process: (amount) => Promise.resolve({ status: 'success', message: 'Zahlung erfolgreich' }) },
];
processMultiplePayments(paymentGateways, 100);
Überlegungen für globale Anwendungen:
- Robustheit:
Promise.allSettled()erhöht die Robustheit Ihrer Anwendungen, indem sichergestellt wird, dass alle asynchronen Operationen versucht werden, auch wenn einige fehlschlagen. Dies ist besonders wichtig in verteilten Systemen, in denen Ausfälle häufig sind. - Detaillierte Berichterstattung: Das Ergebnisarray liefert detaillierte Informationen über das Ergebnis jeder Operation, sodass Sie Fehler protokollieren, fehlgeschlagene Operationen wiederholen oder Benutzern spezifisches Feedback geben können.
- Teilerfolg: Sie können die Gesamterfolgsrate leicht bestimmen und entsprechende Maßnahmen ergreifen, basierend auf der Anzahl der erfolgreichen und fehlgeschlagenen Operationen. Sie könnten beispielsweise alternative Zahlungsmethoden anbieten, wenn das primäre Gateway fehlschlägt.
Promise.race(): Das schnellste Ergebnis wählen
Promise.race() nimmt ebenfalls ein iterierbares Objekt von Promises als Eingabe und gibt ein einzelnes Promise zurück. Im Gegensatz zu Promise.all() und Promise.allSettled() wird Promise.race() jedoch abgeschlossen, sobald irgendein der Eingabe-Promises abgeschlossen ist (entweder erfüllt oder abgelehnt). Das zurückgegebene Promise wird mit dem Wert oder Grund des ersten abgeschlossenen Promises erfüllt oder abgelehnt.
Anwendungsfall: Wenn Sie die schnellste Antwort aus mehreren Quellen auswählen müssen, ist Promise.race() eine gute Wahl. Stellen Sie sich vor, Sie fragen dieselben Daten von mehreren Servern ab und verwenden die erste Antwort, die Sie erhalten. Dies kann die Leistung und Reaktionsfähigkeit verbessern, insbesondere in Situationen, in denen einige Server vorübergehend nicht verfügbar oder langsamer als andere sind.
async function fetchDataFromServer(serverURL) {
try {
const response = await fetch(serverURL, {signal: AbortSignal.timeout(5000)}); // Füge einen Timeout von 5 Sekunden hinzu
if (!response.ok) {
throw new Error(`Daten von ${serverURL} konnten nicht abgerufen werden`);
}
return await response.json();
} catch (error) {
console.error(`Fehler beim Abrufen von Daten von ${serverURL}: ${error}`);
throw error;
}
}
async function getFastestResponse() {
const serverURLs = [
'https://server1.example.com/data', // Durch echte Server-URLs ersetzen
'https://server2.example.com/data',
'https://server3.example.com/data',
];
try {
const dataPromises = serverURLs.map(serverURL => fetchDataFromServer(serverURL));
const fastestData = await Promise.race(dataPromises);
console.log('Schnellste empfangene Daten:', fastestData);
// Die schnellsten Daten verwenden
} catch (error) {
console.error('Daten konnten von keinem Server abgerufen werden:', error);
// Den Fehler behandeln
}
}
getFastestResponse();
Überlegungen für globale Anwendungen:
- Timeouts: Es ist entscheidend, Timeouts zu implementieren, wenn Sie
Promise.race()verwenden, um zu verhindern, dass das zurückgegebene Promise unendlich wartet, falls einige der Eingabe-Promises nie abgeschlossen werden. Das obige Beispiel verwendet `AbortSignal.timeout()`, um dies zu erreichen. - Netzwerkbedingungen: Der schnellste Server kann je nach geografischem Standort des Benutzers und den Netzwerkbedingungen variieren. Erwägen Sie die Verwendung eines Content Delivery Network (CDN), um Ihre Inhalte zu verteilen und die Leistung für Benutzer auf der ganzen Welt zu verbessern.
- Fehlerbehandlung: Wenn das Promise, das das Rennen 'gewinnt', abgelehnt wird, wird das gesamte Promise.race abgelehnt. Stellen Sie sicher, dass jedes Promise eine angemessene Fehlerbehandlung hat, um unerwartete Ablehnungen zu vermeiden. Wenn das "gewinnende" Promise aufgrund eines Timeouts (wie oben gezeigt) abgelehnt wird, werden die anderen Promises im Hintergrund weiter ausgeführt. Möglicherweise müssen Sie Logik hinzufügen, um diese anderen Promises mit `AbortController` abzubrechen, wenn sie nicht mehr benötigt werden.
Promise.any(): Die erste Erfüllung akzeptieren
Promise.any() ähnelt Promise.race(), hat aber ein etwas anderes Verhalten. Es wartet auf das erste erfüllte Eingabe-Promise. Wenn alle Eingabe-Promises abgelehnt werden, wird Promise.any() mit einem AggregateError abgelehnt, der ein Array der Ablehnungsgründe enthält.
Anwendungsfall: Wenn Sie Daten aus mehreren Quellen abrufen müssen und sich nur für das erste erfolgreiche Ergebnis interessieren, ist Promise.any() eine gute Wahl. Dies ist nützlich, wenn Sie redundante Datenquellen oder alternative APIs haben, die die gleichen Informationen liefern. Es priorisiert den Erfolg über die Geschwindigkeit, da es auf die erste Erfüllung wartet, auch wenn einige Promises schnell abgelehnt werden.
async function fetchDataFromSource(sourceURL) {
try {
const response = await fetch(sourceURL);
if (!response.ok) {
throw new Error(`Daten von ${sourceURL} konnten nicht abgerufen werden`);
}
return await response.json();
} catch (error) {
console.error(`Fehler beim Abrufen von Daten von ${sourceURL}: ${error}`);
throw error;
}
}
async function getFirstSuccessfulData() {
const dataSources = [
'https://source1.example.com/data', // Durch echte Datenquellen-URLs ersetzen
'https://source2.example.com/data',
'https://source3.example.com/data',
];
try {
const dataPromises = dataSources.map(sourceURL => fetchDataFromSource(sourceURL));
const data = await Promise.any(dataPromises);
console.log('Erste erfolgreiche Daten empfangen:', data);
// Die erfolgreichen Daten verwenden
} catch (error) {
if (error instanceof AggregateError) {
console.error('Daten konnten von keiner Quelle abgerufen werden:', error.errors);
// Den Fehler behandeln
} else {
console.error('Ein unerwarteter Fehler ist aufgetreten:', error);
}
}
}
getFirstSuccessfulData();
Überlegungen für globale Anwendungen:
- Redundanz:
Promise.any()ist besonders nützlich, wenn es um redundante Datenquellen geht, die ähnliche Informationen liefern. Wenn eine Quelle nicht verfügbar oder langsam ist, können Sie sich auf die anderen verlassen, um die Daten bereitzustellen. - Fehlerbehandlung: Stellen Sie sicher, dass Sie den
AggregateErrorbehandeln, der geworfen wird, wenn alle Eingabe-Promises abgelehnt werden. Dieser Fehler enthält ein Array der einzelnen Ablehnungsgründe, was Ihnen bei der Fehlersuche und Diagnose der Probleme hilft. - Priorisierung: Die Reihenfolge, in der Sie die Promises an
Promise.any()übergeben, ist wichtig. Platzieren Sie die zuverlässigsten oder schnellsten Datenquellen an erster Stelle, um die Wahrscheinlichkeit eines erfolgreichen Ergebnisses zu erhöhen.
Den richtigen Combinator wählen: Eine Zusammenfassung
Hier ist eine kurze Zusammenfassung, die Ihnen hilft, den passenden Promise Combinator für Ihre Bedürfnisse auszuwählen:
- Promise.all(): Verwenden Sie dies, wenn alle Promises erfolgreich erfüllt werden müssen und Sie sofort fehlschlagen möchten, wenn ein Promise abgelehnt wird.
- Promise.allSettled(): Verwenden Sie dies, wenn Sie warten möchten, bis alle Promises abgeschlossen sind, unabhängig von Erfolg oder Misserfolg, und Sie detaillierte Informationen über jedes Ergebnis benötigen.
- Promise.race(): Verwenden Sie dies, wenn Sie das schnellste Ergebnis aus mehreren Promises auswählen möchten und sich nur für das erste interessieren, das abgeschlossen wird.
- Promise.any(): Verwenden Sie dies, wenn Sie das erste erfolgreiche Ergebnis aus mehreren Promises akzeptieren möchten und es Ihnen nichts ausmacht, wenn einige Promises abgelehnt werden.
Fortgeschrittene Muster und Best Practices
Über die grundlegende Verwendung von Promise Combinators hinaus gibt es mehrere fortgeschrittene Muster und Best Practices, die man beachten sollte:
Concurrency begrenzen
Wenn Sie mit einer großen Anzahl von Promises arbeiten, kann die parallele Ausführung aller Promises Ihr System überlasten oder API-Ratenbegrenzungen überschreiten. Sie können die Concurrency mit Techniken wie den folgenden begrenzen:
- Chunking (Aufteilen): Teilen Sie die Promises in kleinere Blöcke (Chunks) auf und verarbeiten Sie jeden Block sequenziell.
- Verwendung eines Semaphors: Implementieren Sie einen Semaphor, um die Anzahl der gleichzeitigen Operationen zu steuern.
Hier ist ein Beispiel mit Chunking:
async function processInChunks(promises, chunkSize) {
const results = [];
for (let i = 0; i < promises.length; i += chunkSize) {
const chunk = promises.slice(i, i + chunkSize);
const chunkResults = await Promise.all(chunk);
results.push(...chunkResults);
}
return results;
}
// Beispielverwendung
const myPromises = [...Array(100)].map((_, i) => Promise.resolve(i)); // Erstelle 100 Promises
processInChunks(myPromises, 10) // Verarbeite 10 Promises auf einmal
.then(results => console.log('Alle Promises aufgelöst:', results));
Fehler elegant behandeln
Eine ordnungsgemäße Fehlerbehandlung ist bei der Arbeit mit Promises entscheidend. Verwenden Sie try...catch-Blöcke, um Fehler abzufangen, die während asynchroner Operationen auftreten können. Erwägen Sie die Verwendung von Bibliotheken wie p-retry oder retry, um fehlgeschlagene Operationen automatisch zu wiederholen.
async function fetchDataWithRetry(url, retries = 3) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP-Fehler! Status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (retries > 0) {
console.log(`Wiederhole in 1 Sekunde... (Verbleibende Versuche: ${retries})`);
await new Promise(resolve => setTimeout(resolve, 1000)); // 1 Sekunde warten
return fetchDataWithRetry(url, retries - 1);
} else {
console.error('Maximale Anzahl an Wiederholungsversuchen erreicht. Operation fehlgeschlagen.');
throw error;
}
}
}
Verwendung von Async/Await
async und await bieten eine synchroner aussehende Möglichkeit, mit Promises zu arbeiten. Sie können die Lesbarkeit und Wartbarkeit des Codes erheblich verbessern.
Denken Sie daran, try...catch-Blöcke um await-Ausdrücke zu verwenden, um potenzielle Fehler zu behandeln.
Abbruch (Cancellation)
In einigen Szenarien müssen Sie möglicherweise ausstehende Promises abbrechen, insbesondere bei lang andauernden Operationen oder vom Benutzer initiierten Aktionen. Sie können die AbortController-API verwenden, um zu signalisieren, dass ein Promise abgebrochen werden soll.
const controller = new AbortController();
const signal = controller.signal;
async function fetchDataWithCancellation(url) {
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP-Fehler! Status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch abgebrochen');
} else {
console.error('Fehler beim Abrufen der Daten:', error);
}
throw error;
}
}
fetchDataWithCancellation('https://api.example.com/data')
.then(data => console.log('Daten empfangen:', data))
.catch(error => console.error('Fetch fehlgeschlagen:', error));
// Breche die Fetch-Operation nach 5 Sekunden ab
setTimeout(() => {
controller.abort();
}, 5000);
Fazit
JavaScript Promise Combinators sind leistungsstarke Werkzeuge für die Erstellung robuster und effizienter asynchroner Anwendungen. Durch das Verständnis der Nuancen von Promise.all, Promise.allSettled, Promise.race und Promise.any können Sie komplexe asynchrone Arbeitsabläufe orchestrieren, Fehler elegant behandeln und die Leistung optimieren. Bei der Entwicklung globaler Anwendungen ist die Berücksichtigung von Netzwerklatenz, API-Ratenbegrenzungen und der Zuverlässigkeit von Datenquellen entscheidend. Durch die Anwendung der in diesem Artikel besprochenen Muster und Best Practices können Sie JavaScript-Anwendungen erstellen, die sowohl leistungsstark als auch widerstandsfähig sind und Benutzern auf der ganzen Welt eine überlegene Benutzererfahrung bieten.