Promise.resolve(): Erstellen eines Promises, das mit einem gegebenen Wert erfüllt wirdPromise.reject(): Erstellen eines Promises, das mit einem gegebenen Wert abgelehnt wird.then()-Callbacks.catch() und sein Callback.finally() [ES2018]XMLHttpRequest promisfizierenutil.promisify()Promise.all()Promise.race()Promise.any() und AggregateError [ES2021]Promise.allSettled() [ES2020]Promise.all() (Fortgeschritten)Promise.all() ist Fork-JoinPromise.all()Promise.race()Promise.any() [ES2021]Promise.allSettled() [ES2020] Empfohlene Lektüre
Dieses Kapitel baut auf dem vorherigen Kapitel mit Hintergrundinformationen zur asynchronen Programmierung in JavaScript auf.
Promises sind eine Technik zur asynchronen Lieferung von Ergebnissen.
Der folgende Code ist ein Beispiel für die Verwendung der Promise-basierten Funktion addAsync() (deren Implementierung bald gezeigt wird)
addAsync(3, 4)
.then(result => { // success
assert.equal(result, 7);
})
.catch(error => { // failure
assert.fail(error);
});Promises ähneln dem Event-Muster: Es gibt ein Objekt (ein Promise), bei dem wir Callbacks registrieren
.then() registriert Callbacks, die Ergebnisse verarbeiten..catch() registriert Callbacks, die Fehler verarbeiten.Eine Promise-basierte Funktion gibt ein Promise zurück und sendet ihm ein Ergebnis oder einen Fehler (sobald sie fertig ist). Das Promise leitet es an die entsprechenden Callbacks weiter.
Im Gegensatz zum Event-Muster sind Promises für einmalige Ergebnisse optimiert
.then() und .catch() verketten, da sie beide Promises zurückgeben. Das hilft bei der sequenziellen Ausführung mehrerer asynchroner Funktionen. Mehr dazu später.Was ist ein Promise? Es gibt zwei Sichtweisen darauf
Dies ist eine Implementierung einer Promise-basierten Funktion, die zwei Zahlen x und y addiert
function addAsync(x, y) {
return new Promise(
(resolve, reject) => { // (A)
if (x === undefined || y === undefined) {
reject(new Error('Must provide two parameters'));
} else {
resolve(x + y);
}
});
}addAsync() ruft sofort den Promise-Konstruktor auf. Die eigentliche Implementierung dieser Funktion befindet sich im Callback, der diesem Konstruktor übergeben wird (Zeile A). Dieser Callback erhält zwei Funktionen
resolve wird zum Liefern eines Ergebnisses (bei Erfolg) verwendet.reject wird zum Liefern eines Fehlers (bei Misserfolg) verwendet.Abb. 22 zeigt die drei Zustände, in denen sich ein Promise befinden kann. Promises sind auf einmalige Ergebnisse spezialisiert und schützen uns vor Race Conditions (zu frühes oder zu spätes Registrieren)
.then()-Callback oder einen .catch()-Callback zu früh registrieren, wird er benachrichtigt, sobald ein Promise abgeschlossen ist..then() oder .catch() nach dem Abschluss aufgerufen werden, erhalten sie den zwischengespeicherten Wert.Darüber hinaus kann sich der Zustand und der Abschlusswert eines Promises nach dem Abschluss nicht mehr ändern. Das hilft, Code vorhersehbar zu machen und die einmalige Natur von Promises zu erzwingen.
Manche Promises werden nie abgeschlossen
Es ist möglich, dass ein Promise nie abgeschlossen wird. Zum Beispiel
new Promise(() => {})Promise.resolve(): Erstellen eines Promises, das mit einem gegebenen Wert erfüllt wirdPromise.resolve(x) erstellt ein Promise, das mit dem Wert x erfüllt wird.
Promise.resolve(123)
.then(x => {
assert.equal(x, 123);
});Wenn der Parameter bereits ein Promise ist, wird es unverändert zurückgegeben.
const abcPromise = Promise.resolve('abc');
assert.equal(
Promise.resolve(abcPromise),
abcPromise);Daher können wir mit Promise.resolve(x) sicherstellen, dass wir ein Promise haben, gegebenenfalls einen beliebigen Wert x.
Beachten Sie, dass der Name resolve und nicht fulfill lautet, da .resolve() ein abgelehntes Promise zurückgibt, wenn sein Parameter ein abgelehntes Promise ist.
Promise.reject(): Erstellen eines Promises, das mit einem gegebenen Wert abgelehnt wirdPromise.reject(err) erstellt ein Promise, das mit dem Wert err abgelehnt wird.
const myError = new Error('My error!');
Promise.reject(myError)
.catch(err => {
assert.equal(err, myError);
});.then()-Callbacks.then() behandelt Promise-Erfüllungen. Es gibt auch ein neues Promise zurück. Wie dieses Promise abgeglichen wird, hängt davon ab, was im Callback passiert. Betrachten wir drei gängige Fälle.
Erstens kann der Callback einen Nicht-Promise-Wert zurückgeben (Zeile A). Folglich wird das von .then() zurückgegebene Promise mit diesem Wert erfüllt (wie in Zeile B geprüft)
Promise.resolve('abc')
.then(str => {
return str + str; // (A)
})
.then(str2 => {
assert.equal(str2, 'abcabc'); // (B)
});Zweitens kann der Callback ein Promise p zurückgeben (Zeile A). Folglich “wird” p zu dem, was .then() zurückgibt. Mit anderen Worten: Das von .then() bereits zurückgegebene Promise wird effektiv durch p ersetzt.
Promise.resolve('abc')
.then(str => {
return Promise.resolve(123); // (A)
})
.then(num => {
assert.equal(num, 123);
});Warum ist das nützlich? Wir können das Ergebnis einer Promise-basierten Operation zurückgeben und seinen Erfüllungswert über ein „flaches“ (nicht verschachteltes) .then() verarbeiten. Vergleichen Sie
// Flat
asyncFunc1()
.then(result1 => {
/*···*/
return asyncFunc2();
})
.then(result2 => {
/*···*/
});
// Nested
asyncFunc1()
.then(result1 => {
/*···*/
asyncFunc2()
.then(result2 => {
/*···*/
});
});Drittens kann der Callback eine Ausnahme auslösen. Folglich wird das von .then() zurückgegebene Promise mit dieser Ausnahme abgelehnt. Das heißt, ein synchroner Fehler wird in einen asynchronen Fehler umgewandelt.
const myError = new Error('My error!');
Promise.resolve('abc')
.then(str => {
throw myError;
})
.catch(err => {
assert.equal(err, myError);
});.catch() und sein CallbackDer Unterschied zwischen .then() und .catch() besteht darin, dass letzteres durch Ablehnungen und nicht durch Erfüllungen ausgelöst wird. Beide Methoden wandeln die Aktionen ihrer Callbacks jedoch auf die gleiche Weise in Promises um. Zum Beispiel wird in der folgenden Code der von der .catch()-Callback in Zeile A zurückgegebene Wert ein Erfüllungswert
const err = new Error();
Promise.reject(err)
.catch(e => {
assert.equal(e, err);
// Something went wrong, use a default value
return 'default value'; // (A)
})
.then(str => {
assert.equal(str, 'default value');
});.then() und .catch() geben immer Promises zurück. Das ermöglicht es uns, beliebig lange Ketten von Methodenaufrufen zu erstellen
function myAsyncFunc() {
return asyncFunc1() // (A)
.then(result1 => {
// ···
return asyncFunc2(); // a Promise
})
.then(result2 => {
// ···
return result2 ?? '(Empty)'; // not a Promise
})
.then(result3 => {
// ···
return asyncFunc4(); // a Promise
});
}Aufgrund der Verkettung gibt das return in Zeile A das Ergebnis des letzten .then() zurück.
In gewisser Weise ist .then() die asynchrone Version des synchronen Semikolons
.then() führt zwei asynchrone Operationen sequenziell aus.Wir können auch .catch() in die Mischung einbeziehen und es mehrere Fehlerquellen gleichzeitig verarbeiten lassen
asyncFunc1()
.then(result1 => {
// ···
return asyncFunction2();
})
.then(result2 => {
// ···
})
.catch(error => {
// Failure: handle errors of asyncFunc1(), asyncFunc2()
// and any (sync) exceptions thrown in previous callbacks
});.finally() [ES2018]Die Promise-Methode .finally() wird oft wie folgt verwendet
somePromise
.then((result) => {
// ···
})
.catch((error) => {
// ···
})
.finally(() => {
// ···
})
;Der .finally()-Callback wird immer ausgeführt – unabhängig von somePromise und den von .then() und/oder .catch() zurückgegebenen Werten. Im Gegensatz dazu
.then()-Callback wird nur ausgeführt, wenn somePromise erfüllt wird..catch()-Callback wird nur ausgeführt, wennsomePromise abgelehnt wird,.then()-Callback ein abgelehntes Promise zurückgibt,.then()-Callback eine Ausnahme auslöst..finally() ignoriert, was sein Callback zurückgibt, und gibt einfach die vorherige Abgleichung weiter
Promise.resolve(123)
.finally(() => {})
.then((result) => {
assert.equal(result, 123);
});
Promise.reject('error')
.finally(() => {})
.catch((error) => {
assert.equal(error, 'error');
});Wenn jedoch der .finally()-Callback eine Ausnahme auslöst, wird das von .finally() zurückgegebene Promise abgelehnt.
Promise.reject('error (originally)')
.finally(() => {
throw 'error (finally)';
})
.catch((error) => {
assert.equal(error, 'error (finally)');
});.finally(): BereinigungEin häufiger Anwendungsfall für .finally() ähnelt einem häufigen Anwendungsfall der synchronen finally-Klausel: die Bereinigung, nachdem Sie mit einer Ressource fertig sind. Dies sollte immer geschehen, unabhängig davon, ob alles reibungslos verlief oder ein Fehler aufgetreten ist – zum Beispiel
let connection;
db.open()
.then((conn) => {
connection = conn;
return connection.select({ name: 'Jane' });
})
.then((result) => {
// Process result
// Use `connection` to make more queries
})
// ···
.catch((error) => {
// handle errors
})
.finally(() => {
connection.close();
});.finally(): Etwas zuerst nach jeder Art von Abgleichung tunWir können .finally() auch vor sowohl .then() als auch .catch() verwenden. Dann wird das, was wir im .finally()-Callback tun, immer vor den anderen beiden Callbacks ausgeführt.
Beispielsweise geschieht dies mit einem erfüllten Promise
Promise.resolve('fulfilled')
.finally(() => {
console.log('finally');
})
.then((result) => {
console.log('then ' + result);
})
.catch((error) => {
console.log('catch ' + error);
})
;
// Output:
// 'finally'
// 'then fulfilled'Dies geschieht mit einem abgelehnten Promise
Promise.reject('rejected')
.finally(() => {
console.log('finally');
})
.then((result) => {
console.log('then ' + result);
})
.catch((error) => {
console.log('catch ' + error);
})
;
// Output:
// 'finally'
// 'catch rejected'Dies sind einige der Vorteile von Promises gegenüber einfachen Callbacks bei der Verarbeitung von einmaligen Ergebnissen
Die Typsignaturen von Promise-basierten Funktionen und Methoden sind sauberer: Wenn eine Funktion Callback-basiert ist, beziehen sich einige Parameter auf die Eingabe, während die ein oder zwei Callbacks am Ende auf die Ausgabe bezogen sind. Bei Promises wird alles Ausgaberelevante über den Rückgabewert abgewickelt.
Das Verketten asynchroner Verarbeitungsschritte ist bequemer.
Promises verarbeiten sowohl asynchrone Fehler (über Ablehnungen) als auch synchrone Fehler: Innerhalb der Callbacks für new Promise(), .then() und .catch() werden Ausnahmen in Ablehnungen umgewandelt. Wenn wir jedoch Callbacks für Asynchronität verwenden, werden Ausnahmen normalerweise nicht für uns behandelt; wir müssen uns selbst darum kümmern.
Promises sind ein einziger Standard, der langsam mehrere, sich gegenseitig ausschließende Alternativen ersetzt. Zum Beispiel sind in Node.js viele Funktionen jetzt in Promise-basierten Versionen verfügbar. Und neue asynchrone Browser-APIs sind in der Regel Promise-basiert.
Einer der größten Vorteile von Promises liegt darin, dass man nicht direkt mit ihnen arbeitet: Sie sind die Grundlage von Async-Funktionen, einer synchron aussehenden Syntax zur Durchführung asynchroner Berechnungen. Asynchrone Funktionen werden im nächsten Kapitel behandelt.
Promises in Aktion zu sehen hilft beim Verstehen. Werfen wir einen Blick auf Beispiele.
Betrachten Sie die folgende Textdatei person.json mit JSON-Daten darin
{
"first": "Jane",
"last": "Doe"
}Werfen wir einen Blick auf zwei Versionen von Code, die diese Datei lesen und in ein Objekt parsen. Erstens, eine Callback-basierte Version. Zweitens, eine Promise-basierte Version.
Der folgende Code liest den Inhalt dieser Datei und wandelt ihn in ein JavaScript-Objekt um. Er basiert auf Node.js-typischen Callbacks
import * as fs from 'fs';
fs.readFile('person.json',
(error, text) => {
if (error) { // (A)
// Failure
assert.fail(error);
} else {
// Success
try { // (B)
const obj = JSON.parse(text); // (C)
assert.deepEqual(obj, {
first: 'Jane',
last: 'Doe',
});
} catch (e) {
// Invalid JSON
assert.fail(e);
}
}
});fs ist ein integriertes Node.js-Modul für Dateisystemoperationen. Wir verwenden die Callback-basierte Funktion fs.readFile(), um eine Datei namens person.json zu lesen. Wenn wir erfolgreich sind, wird der Inhalt über den Parameter text als String geliefert. In Zeile C wandeln wir diesen String vom textbasierten Datenformat JSON in ein JavaScript-Objekt um. JSON ist ein Objekt mit Methoden zum Konsumieren und Produzieren von JSON. Es ist Teil der Standardbibliothek von JavaScript und wird später in diesem Buch dokumentiert.
Beachten Sie, dass es zwei Fehlerbehandlungsmechanismen gibt: Das if in Zeile A kümmert sich um asynchrone Fehler, die von fs.readFile() gemeldet werden, während das try in Zeile B sich um synchrone Fehler kümmert, die von JSON.parse() gemeldet werden.
Der folgende Code verwendet readFileAsync(), eine Promise-basierte Version von fs.readFile() (erstellt über util.promisify(), das später erklärt wird)
readFileAsync('person.json')
.then(text => { // (A)
// Success
const obj = JSON.parse(text);
assert.deepEqual(obj, {
first: 'Jane',
last: 'Doe',
});
})
.catch(err => { // (B)
// Failure: file I/O error or JSON syntax error
assert.fail(err);
});Die Funktion readFileAsync() gibt ein Promise zurück. In Zeile A geben wir einen Erfolgs-Callback über die Methode .then() dieses Promises an. Der restliche Code im then-Callback ist synchron.
.then() gibt ein Promise zurück, das die Ausführung der Promise-Methode .catch() in Zeile B ermöglicht. Wir verwenden es, um einen Fehler-Callback anzugeben.
Beachten Sie, dass .catch() uns ermöglicht, sowohl die asynchronen Fehler von readFileAsync() als auch die synchronen Fehler von JSON.parse() zu behandeln, da Ausnahmen innerhalb eines .then()-Callbacks zu Ablehnungen werden.
XMLHttpRequest promisfizierenWir haben zuvor die ereignisbasierte XMLHttpRequest-API zum Herunterladen von Daten in Webbrowsern gesehen. Die folgende Funktion promisfiziert diese API
function httpGet(url) {
return new Promise(
(resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.responseText); // (A)
} else {
// Something went wrong (404, etc.)
reject(new Error(xhr.statusText)); // (B)
}
}
xhr.onerror = () => {
reject(new Error('Network error')); // (C)
};
xhr.open('GET', url);
xhr.send();
});
}Beachten Sie, wie die Ergebnisse und Fehler von XMLHttpRequest über resolve() und reject() behandelt werden.
So verwenden Sie httpGet()
httpGet('http://example.com/textfile.txt')
.then(content => {
assert.equal(content, 'Content of textfile.txt\n');
})
.catch(error => {
assert.fail(error);
}); Übung: Timeout für ein Promise
exercises/promises/promise_timeout_test.mjs
util.promisify()util.promisify() ist eine Hilfsfunktion, die eine Callback-basierte Funktion f in eine Promise-basierte Funktion umwandelt. Das heißt, wir gehen von dieser Typsignatur aus
f(arg_1, ···, arg_n, (err: Error, result: T) => void) : void
Zu dieser Typsignatur
f(arg_1, ···, arg_n) : Promise<T>
Der folgende Code promisfiziert die Callback-basierte Funktion fs.readFile() (Zeile A) und verwendet sie.
import * as fs from 'fs';
import {promisify} from 'util';
const readFileAsync = promisify(fs.readFile); // (A)
readFileAsync('some-file.txt', {encoding: 'utf8'})
.then(text => {
assert.equal(text, 'The content of some-file.txt\n');
})
.catch(err => {
assert.fail(err);
}); Übungen:
util.promisify()
util.promisify(): exercises/promises/read_file_async_exrc.mjsutil.promisify() selbst: exercises/promises/my_promisify_test.mjsAlle modernen Browser unterstützen Fetch, eine neue Promise-basierte API zum Herunterladen von Daten. Betrachten Sie sie als eine Promise-basierte Version von XMLHttpRequest. Das Folgende ist ein Auszug aus der API
interface Body {
text() : Promise<string>;
···
}
interface Response extends Body {
···
}
declare function fetch(str) : Promise<Response>;Das bedeutet, dass wir fetch() wie folgt verwenden können
fetch('http://example.com/textfile.txt')
.then(response => response.text())
.then(text => {
assert.equal(text, 'Content of textfile.txt\n');
}); Übung: Verwendung der Fetch API
exercises/promises/fetch_json_test.mjs
Regel für die Implementierung von Funktionen und Methoden
Vermischen Sie keine (asynchronen) Ablehnungen und (synchronen) Ausnahmen.
Dies macht unseren synchronen und asynchronen Code vorhersehbarer und einfacher, da wir uns immer auf einen einzigen Fehlerbehandlungsmechanismus konzentrieren können.
Für Promise-basierte Funktionen und Methoden bedeutet die Regel, dass sie niemals Ausnahmen auslösen sollten. Leider ist es leicht, dies versehentlich falsch zu machen – zum Beispiel
// Don’t do this
function asyncFunc() {
doSomethingSync(); // (A)
return doSomethingAsync()
.then(result => {
// ···
});
}Das Problem ist, dass wenn in Zeile A eine Ausnahme ausgelöst wird, asyncFunc() eine Ausnahme auslöst. Aufrufer dieser Funktion erwarten nur Ablehnungen und sind nicht auf eine Ausnahme vorbereitet. Es gibt drei Möglichkeiten, dieses Problem zu beheben.
Wir können den gesamten Körper der Funktion in eine try-catch-Anweisung einpacken und ein abgelehntes Promise zurückgeben, wenn eine Ausnahme ausgelöst wird
// Solution 1
function asyncFunc() {
try {
doSomethingSync();
return doSomethingAsync()
.then(result => {
// ···
});
} catch (err) {
return Promise.reject(err);
}
}Da .then() Ausnahmen in Ablehnungen umwandelt, können wir doSomethingSync() innerhalb eines .then()-Callbacks ausführen. Dazu starten wir eine Promise-Kette über Promise.resolve(). Wir ignorieren den Erfüllungswert undefined dieses anfänglichen Promises.
// Solution 2
function asyncFunc() {
return Promise.resolve()
.then(() => {
doSomethingSync();
return doSomethingAsync();
})
.then(result => {
// ···
});
}Schließlich wandelt auch new Promise() Ausnahmen in Ablehnungen um. Die Verwendung dieses Konstruktors ähnelt daher der vorherigen Lösung
// Solution 3
function asyncFunc() {
return new Promise((resolve, reject) => {
doSomethingSync();
resolve(doSomethingAsync());
})
.then(result => {
// ···
});
}Die meisten Promise-basierten Funktionen werden wie folgt ausgeführt
Der folgende Code demonstriert dies
function asyncFunc() {
console.log('asyncFunc');
return new Promise(
(resolve, _reject) => {
console.log('new Promise()');
resolve();
});
}
console.log('START');
asyncFunc()
.then(() => {
console.log('.then()'); // (A)
});
console.log('END');
// Output:
// 'START'
// 'asyncFunc'
// 'new Promise()'
// 'END'
// '.then()'Wir sehen, dass der Callback von new Promise() vor dem Ende des Codes ausgeführt wird, während das Ergebnis später geliefert wird (Zeile A).
Vorteile dieses Ansatzes
Das synchrone Starten hilft, Race Conditions zu vermeiden, da wir uns auf die Reihenfolge verlassen können, in der Promise-basierte Funktionen beginnen. Es gibt ein Beispiel im nächsten Kapitel, wo Text in eine Datei geschrieben und Race Conditions vermieden werden.
Das Verketten von Promises verhungert andere Tasks nicht mit Verarbeitungszeit, da es immer eine Pause gibt, bevor ein Promise abgeschlossen wird, während der die Event-Schleife laufen kann.
Promise-basierte Funktionen geben immer asynchron Ergebnisse zurück; wir können sicher sein, dass es nie eine synchrone Rückgabe gibt. Diese Art von Vorhersehbarkeit macht die Arbeit mit Code einfacher.
Weitere Informationen zu diesem Ansatz
„Designing APIs for Asynchrony“ von Isaac Z. Schlueter
Das Kombinator-Muster ist ein Muster in der funktionalen Programmierung zum Erstellen von Strukturen. Es basiert auf zwei Arten von Funktionen
Wenn es um JavaScript Promises geht
Primitive Funktionen umfassen: Promise.resolve(), Promise.reject()
Kombinatoren umfassen: Promise.all(), Promise.race(), Promise.any(), Promise.allSettled(). In jedem dieser Fälle
Als Nächstes werden wir die genannten Promise-Kombinatoren genauer betrachten.
Promise.all()Dies ist die Typsignatur von Promise.all()
Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>Promise.all() gibt ein Promise zurück, das
promises erfüllt sind.promises.Dies ist eine schnelle Demo des Erfüllungs-Promises, das erfüllt wird
const promises = [
Promise.resolve('result a'),
Promise.resolve('result b'),
Promise.resolve('result c'),
];
Promise.all(promises)
.then((arr) => assert.deepEqual(
arr, ['result a', 'result b', 'result c']
));Das folgende Beispiel zeigt, was passiert, wenn mindestens eines der Eingabe-Promises abgelehnt wird
const promises = [
Promise.resolve('result a'),
Promise.resolve('result b'),
Promise.reject('ERROR'),
];
Promise.all(promises)
.catch((err) => assert.equal(
err, 'ERROR'
));Abb. 23 veranschaulicht, wie Promise.all() funktioniert.
.map() über Promise.all()Array-Transformationsmethoden wie .map(), .filter() usw. sind für synchrone Berechnungen gedacht. Zum Beispiel
function timesTwoSync(x) {
return 2 * x;
}
const arr = [1, 2, 3];
const result = arr.map(timesTwoSync);
assert.deepEqual(result, [2, 4, 6]);Was passiert, wenn der Callback von .map() eine Promise-basierte Funktion ist (eine Funktion, die normale Werte auf Promises abbildet)? Dann ist das Ergebnis von .map() ein Array von Promises. Leider sind das keine Daten, mit denen normaler Code arbeiten kann. Glücklicherweise können wir das mit Promise.all() beheben: Es konvertiert ein Array von Promises in ein Promise, das mit einem Array normaler Werte erfüllt wird.
function timesTwoAsync(x) {
return new Promise(resolve => resolve(x * 2));
}
const arr = [1, 2, 3];
const promiseArr = arr.map(timesTwoAsync);
Promise.all(promiseArr)
.then(result => {
assert.deepEqual(result, [2, 4, 6]);
});.map()-BeispielAls Nächstes verwenden wir .map() und Promise.all(), um Textdateien aus dem Web herunterzuladen. Dafür benötigen wir die folgende Hilfsfunktion
function downloadText(url) {
return fetch(url)
.then((response) => { // (A)
if (!response.ok) { // (B)
throw new Error(response.statusText);
}
return response.text(); // (C)
});
}downloadText() verwendet die Promise-basierte Fetch-API, um eine Textdatei als String herunterzuladen.
response ab (Zeile A).response.ok (Zeile B) prüft, ob Fehler wie „Datei nicht gefunden“ vorlagen..text() (Zeile C), um den Inhalt der Datei als String abzurufen.Im folgenden Beispiel laden wir zwei Textdateien herunter
const urls = [
'http://example.com/first.txt',
'http://example.com/second.txt',
];
const promises = urls.map(
url => downloadText(url));
Promise.all(promises)
.then(
(arr) => assert.deepEqual(
arr, ['First!', 'Second!']
));Promise.all()Dies ist eine vereinfachte Implementierung von Promise.all() (z. B. führt sie keine Sicherheitsprüfungen durch)
function all(iterable) {
return new Promise((resolve, reject) => {
let elementCount = 0;
let result;
let index = 0;
for (const promise of iterable) {
// Preserve the current value of `index`
const currentIndex = index;
promise.then(
(value) => {
result[currentIndex] = value;
elementCount++;
if (elementCount === result.length) {
resolve(result); // (A)
}
},
(err) => {
reject(err); // (B)
});
index++;
}
if (index === 0) {
resolve([]);
return;
}
// Now we know how many Promises there are in `iterable`.
// We can wait until now with initializing `result` because
// the callbacks of .then() are executed asynchronously.
result = new Array(index);
});
}Die beiden Hauptstellen, an denen das Ergebnis-Promise abgeglichen wird, sind Zeile A und Zeile B. Nachdem eine davon abgeglichen wurde, kann die andere den Abgleichungswert nicht mehr ändern, da ein Promise nur einmal abgeglichen werden kann.
Promise.race()Dies ist die Typsignatur von Promise.race()
Promise.race<T>(promises: Iterable<Promise<T>>): Promise<T>Promise.race() gibt ein Promise q zurück, das abgeglichen wird, sobald das erste Promise p unter promises abgeglichen wird. q hat denselben Abgleichungswert wie p.
In der folgenden Demo geschieht die Abgleichung des erfüllten Promises (Zeile A) vor der Abgleichung des abgelehnten Promises (Zeile B). Daher ist das Ergebnis auch erfüllt (Zeile C).
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 100)), // (A)
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 200)), // (B)
];
Promise.race(promises)
.then((result) => assert.equal( // (C)
result, 'result'));In der nächsten Demo geschieht die Ablehnung zuerst
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 200)),
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 100)),
];
Promise.race(promises)
.then(
(result) => assert.fail(),
(err) => assert.equal(
err, 'ERROR'));Beachten Sie, dass das von Promise.race() zurückgegebene Promise so bald wie das erste seiner Eingabe-Promises abgeglichen wird, abgeglichen wird. Das bedeutet, dass das Ergebnis von Promise.race([]) nie abgeglichen wird.
Abb. 24 veranschaulicht, wie Promise.race() funktioniert.
Promise.race() zum Timeout eines PromisesIn diesem Abschnitt verwenden wir Promise.race(), um Promises mit einem Timeout zu versehen. Die folgende Hilfsfunktion wird mehrmals nützlich sein
function resolveAfter(ms, value=undefined) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(value), ms);
});
}resolveAfter() gibt ein Promise zurück, das nach ms Millisekunden mit value aufgelöst wird.
Diese Funktion macht ein Promise zu einem Timeout
function timeout(timeoutInMs, promise) {
return Promise.race([
promise,
resolveAfter(timeoutInMs,
Promise.reject(new Error('Operation timed out'))),
]);
}timeout() gibt ein Promise zurück, dessen Abgleichung dieselbe ist wie die, welches der beiden Promises zuerst abgeglichen wird
promisetimeoutInMs Millisekunden abgelehnt wird.Um das zweite Promise zu erzeugen, nutzt timeout() die Tatsache, dass das Auflösen eines ausstehenden Promises mit einem abgelehnten Promise dazu führt, dass das erstere abgelehnt wird.
Lassen Sie uns timeout() in Aktion sehen. Hier wird das Eingabe-Promise vor dem Timeout erfüllt. Daher wird das Ausgabe-Promise erfüllt.
timeout(200, resolveAfter(100, 'Result!'))
.then(result => assert.equal(result, 'Result!'));Hier geschieht das Timeout, bevor das Eingabe-Promise erfüllt wird. Daher wird das Ausgabe-Promise abgelehnt.
timeout(100, resolveAfter(2000, 'Result!'))
.catch(err => assert.deepEqual(err, new Error('Operation timed out')));Es ist wichtig zu verstehen, was „Timeout eines Promises“ wirklich bedeutet
Das heißt, das Timeout verhindert nur, dass das Eingabe-Promise das Ausgabe-Promise beeinflusst (da ein Promise nur einmal abgeglichen werden kann). Aber es stoppt nicht die asynchrone Operation, die das Eingabe-Promise erzeugt hat.
Promise.race()Dies ist eine vereinfachte Implementierung von Promise.race() (z. B. führt sie keine Sicherheitsprüfungen durch)
function race(iterable) {
return new Promise((resolve, reject) => {
for (const promise of iterable) {
promise.then(
(value) => {
resolve(value); // (A)
},
(err) => {
reject(err); // (B)
});
}
});
}Das Ergebnis-Promise wird entweder in Zeile A oder in Zeile B abgeglichen. Sobald dies geschehen ist, kann der Abgleichungswert nicht mehr geändert werden.
Promise.any() und AggregateError [ES2021]Dies ist die Typsignatur von Promise.any()
Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>Promise.any() gibt ein Promise p zurück. Wie es abgeglichen wird, hängt vom Parameter promises ab (der sich auf ein Iterable von Promises bezieht)
p mit diesem Promise aufgelöst.p mit einer Instanz von AggregateError abgelehnt, die alle Ablehnungswerte enthält.Dies ist die Typsignatur von AggregateError (eine Unterklasse von Error)
class AggregateError extends Error {
// Instance properties (complementing the ones of Error)
errors: Array<any>;
constructor(
errors: Iterable<any>,
message: string = '',
options?: ErrorOptions // ES2022
);
}
interface ErrorOptions {
cause?: any; // ES2022
}Abb. 25 veranschaulicht, wie Promise.any() funktioniert.
Dies ist, was passiert, wenn ein Promise erfüllt wird
const promises = [
Promise.reject('ERROR A'),
Promise.reject('ERROR B'),
Promise.resolve('result'),
];
Promise.any(promises)
.then((result) => assert.equal(
result, 'result'
));Dies ist, was passiert, wenn alle Promises abgelehnt werden
const promises = [
Promise.reject('ERROR A'),
Promise.reject('ERROR B'),
Promise.reject('ERROR C'),
];
Promise.any(promises)
.catch((aggregateError) => assert.deepEqual(
aggregateError.errors,
['ERROR A', 'ERROR B', 'ERROR C']
));Promise.any() vs. Promise.all()Es gibt zwei Möglichkeiten, wie Promise.any() und Promise.all() verglichen werden können
Promise.all(): Erste Eingabeablehnung lehnt das Ergebnis-Promise ab oder sein Erfüllungswert ist ein Array mit Eingabeerfüllungswerten.Promise.any(): Erste Eingabeerfüllung erfüllt das Ergebnis-Promise oder sein Ablehnungswert ist ein Array mit Eingabeablehnungswerten (innerhalb eines Fehlerobjekts).Promise.all() interessiert sich für alle Erfüllungen. Der gegenteilige Fall (mindestens eine Ablehnung) führt zu einer Ablehnung.Promise.any() interessiert sich für die erste Erfüllung. Der gegenteilige Fall (nur Ablehnungen) führt zu einer Ablehnung.Promise.any() vs. Promise.race()Promise.any() und Promise.race() sind ebenfalls verwandt, interessieren sich aber für unterschiedliche Dinge
Promise.race() interessiert sich für Abgleiche. Das Promise, das zuerst abgeglichen wird, „gewinnt“. Mit anderen Worten: Wir wollen von der asynchronen Berechnung erfahren, die zuerst terminiert.Promise.any() interessiert sich für Erfüllungen. Das Promise, das zuerst erfüllt wird, „gewinnt“. Mit anderen Worten: Wir wollen von der asynchronen Berechnung erfahren, die zuerst erfolgreich ist.Der Haupt – relativ seltene – Anwendungsfall für .race() ist das Timeout von Promises. Die Anwendungsfälle für .any() sind breiter. Wir werden sie als Nächstes betrachten.
Promise.any()Wir verwenden Promise.any(), wenn wir mehrere asynchrone Berechnungen haben und uns nur die erste erfolgreiche interessiert. In gewisser Weise lassen wir die Berechnungen miteinander konkurrieren und verwenden, welche auch immer am schnellsten ist.
Der folgende Code demonstriert, wie das beim Herunterladen von Ressourcen aussieht
const resource = await Promise.any([
fetch('http://example.com/first.txt')
.then(response => response.text()),
fetch('http://example.com/second.txt')
.then(response => response.text()),
]);Das gleiche Muster ermöglicht es uns, welches Modul auch immer schneller herunterlädt
const lodash = await Promise.any([
import('https://primary.example.com/lodash'),
import('https://secondary.example.com/lodash'),
]);Zum Vergleich, dies ist der Code, den wir verwenden würden, wenn der sekundäre Server nur ein Fallback ist – falls der primäre Server fehlschlägt.
let lodash;
try {
lodash = await import('https://primary.example.com/lodash');
} catch {
lodash = await import('https://secondary.example.com/lodash');
}Promise.any() implementieren?Eine einfache Implementierung von Promise.any() ist im Grunde eine Spiegelversion der Implementierung von Promise.all().
Promise.allSettled() [ES2020]Diesmal sind die Typsignaturen etwas komplizierter. Überspringen Sie ruhig zur ersten Demo, die leichter zu verstehen sein sollte.
Dies ist die Typsignatur von Promise.allSettled()
Promise.allSettled<T>(promises: Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>Es gibt ein Promise für ein Array zurück, dessen Elemente die folgende Typsignatur haben
type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;
interface FulfillmentObject<T> {
status: 'fulfilled';
value: T;
}
interface RejectionObject {
status: 'rejected';
reason: unknown;
}Promise.allSettled() gibt ein Promise out zurück. Sobald alle promises abgeglichen sind, wird out mit einem Array erfüllt. Jedes Element e dieses Arrays entspricht einem Promise p von promises
Wenn p mit dem Erfüllungswert v erfüllt wird, dann ist e
{ status: 'fulfilled', value: v }Wenn p mit dem Ablehnungswert r abgelehnt wird, dann ist e
{ status: 'rejected', reason: r }Sofern beim Iterieren über promises kein Fehler auftritt, wird das Ausgabe-Promise out niemals abgelehnt.
Abb. 26 veranschaulicht, wie Promise.allSettled() funktioniert.
Promise.allSettled()Dies ist eine kurze erste Demo, wie Promise.allSettled() funktioniert
Promise.allSettled([
Promise.resolve('a'),
Promise.reject('b'),
])
.then(arr => assert.deepEqual(arr, [
{ status: 'fulfilled', value: 'a' },
{ status: 'rejected', reason: 'b' },
]));Promise.allSettled()Das nächste Beispiel ähnelt dem Beispiel ".map()" plus "Promise.all()" (von dem wir die Funktion downloadText() übernehmen): Wir laden mehrere Textdateien herunter, deren URLs in einem Array gespeichert sind. Diesmal möchten wir jedoch nicht bei einem Fehler aufhören, sondern weitermachen. Promise.allSettled() ermöglicht uns das
const urls = [
'http://example.com/exists.txt',
'http://example.com/missing.txt',
];
const result = Promise.allSettled(
urls.map(u => downloadText(u)));
result.then(
arr => assert.deepEqual(
arr,
[
{
status: 'fulfilled',
value: 'Hello!',
},
{
status: 'rejected',
reason: new Error('Not Found'),
},
]
));Promise.allSettled()Dies ist eine vereinfachte Implementierung von Promise.allSettled() (z. B. führt sie keine Sicherheitsprüfungen durch)
function allSettled(iterable) {
return new Promise((resolve, reject) => {
let elementCount = 0;
let result;
function addElementToResult(i, elem) {
result[i] = elem;
elementCount++;
if (elementCount === result.length) {
resolve(result);
}
}
let index = 0;
for (const promise of iterable) {
// Capture the current value of `index`
const currentIndex = index;
promise.then(
(value) => addElementToResult(
currentIndex, {
status: 'fulfilled',
value
}),
(reason) => addElementToResult(
currentIndex, {
status: 'rejected',
reason
}));
index++;
}
if (index === 0) {
resolve([]);
return;
}
// Now we know how many Promises there are in `iterable`.
// We can wait until now with initializing `result` because
// the callbacks of .then() are executed asynchronously.
result = new Array(index);
});
}Für einen Promise-Kombinator bedeutet Short-Circuiting, dass der Output-Promise vorzeitig beendet wird – bevor alle Input-Promises beendet sind. Die folgenden Kombinatoren führen ein Short-Circuiting durch
Promise.all(): Der Output-Promise wird abgelehnt, sobald ein Input-Promise abgelehnt wird.Promise.race(): Der Output-Promise wird beendet, sobald ein Input-Promise beendet wird.Promise.any(): Der Output-Promise wird erfüllt, sobald ein Input-Promise erfüllt wird.Auch hier bedeutet ein früheres Beenden nicht, dass die Operationen hinter den ignorierten Promises gestoppt werden. Es bedeutet nur, dass ihre Beendigungen ignoriert werden.
Promise.all() (fortgeschritten)Betrachten Sie den folgenden Code
const asyncFunc1 = () => Promise.resolve('one');
const asyncFunc2 = () => Promise.resolve('two');
asyncFunc1()
.then(result1 => {
assert.equal(result1, 'one');
return asyncFunc2();
})
.then(result2 => {
assert.equal(result2, 'two');
});Die Verwendung von .then() auf diese Weise führt Promise-basierte Funktionen sequentiell aus: Erst nachdem das Ergebnis von asyncFunc1() beendet wurde, wird asyncFunc2() ausgeführt.
Promise.all() hilft bei der nebenläufigeren Ausführung von Promise-basierten Funktionen
Promise.all([asyncFunc1(), asyncFunc2()])
.then(arr => {
assert.deepEqual(arr, ['one', 'two']);
});Tipp zur Bestimmung, wie „nebenläufig“ asynchroner Code ist: Konzentrieren Sie sich darauf, wann asynchrone Operationen beginnen, nicht darauf, wie ihre Promises behandelt werden.
Zum Beispiel führt jede der folgenden Funktionen asyncFunc1() und asyncFunc2() nebenläufig aus, da sie nahezu gleichzeitig gestartet werden.
function concurrentAll() {
return Promise.all([asyncFunc1(), asyncFunc2()]);
}
function concurrentThen() {
const p1 = asyncFunc1();
const p2 = asyncFunc2();
return p1.then(r1 => p2.then(r2 => [r1, r2]));
}Auf der anderen Seite führen beide der folgenden Funktionen asyncFunc1() und asyncFunc2() sequentiell aus: asyncFunc2() wird erst aufgerufen, nachdem das Promise von asyncFunc1() erfüllt wurde.
function sequentialThen() {
return asyncFunc1()
.then(r1 => asyncFunc2()
.then(r2 => [r1, r2]));
}
function sequentialAll() {
const p1 = asyncFunc1();
const p2 = p1.then(() => asyncFunc2());
return Promise.all([p1, p2]);
}Promise.all() ist Fork-JoinPromise.all() steht in losem Zusammenhang mit dem Nebenläufigkeitsmuster „Fork-Join“. Betrachten wir noch einmal ein Beispiel, das wir zuvor angetroffen haben.
Promise.all([
// (A) fork
downloadText('http://example.com/first.txt'),
downloadText('http://example.com/second.txt'),
])
// (B) join
.then(
(arr) => assert.deepEqual(
arr, ['First!', 'Second!']
));Dieser Abschnitt gibt Tipps zum Verketten von Promises.
Problem
// Don’t do this
function foo() {
const promise = asyncFunc();
promise.then(result => {
// ···
});
return promise;
}Die Berechnung beginnt mit dem Promise, das von asyncFunc() zurückgegeben wird. Aber danach wird die Berechnung fortgesetzt und über .then() ein weiteres Promise erstellt. foo() gibt das erstere Promise zurück, sollte aber das letztere zurückgeben. So beheben Sie das
function foo() {
const promise = asyncFunc();
return promise.then(result => {
// ···
});
}Problem
// Don’t do this
asyncFunc1()
.then(result1 => {
return asyncFunc2()
.then(result2 => { // (A)
// ···
});
});Das .then() in Zeile A ist verschachtelt. Eine flache Struktur wäre besser
asyncFunc1()
.then(result1 => {
return asyncFunc2();
})
.then(result2 => {
// ···
});Dies ist ein weiteres Beispiel für vermeidbare Verschachtelung
// Don’t do this
asyncFunc1()
.then(result1 => {
if (result1 < 0) {
return asyncFuncA()
.then(resultA => 'Result: ' + resultA);
} else {
return asyncFuncB()
.then(resultB => 'Result: ' + resultB);
}
});Wir können wieder eine flache Struktur erhalten
asyncFunc1()
.then(result1 => {
return result1 < 0 ? asyncFuncA() : asyncFuncB();
})
.then(resultAB => {
return 'Result: ' + resultAB;
});Im folgenden Code profitieren wir tatsächlich von der Verschachtelung
db.open()
.then(connection => { // (A)
return connection.select({ name: 'Jane' })
.then(result => { // (B)
// Process result
// Use `connection` to make more queries
})
// ···
.finally(() => {
connection.close(); // (C)
});
})Wir erhalten in Zeile A ein asynchrones Ergebnis. In Zeile B verschachteln wir, damit wir in der Callback-Funktion und in Zeile C Zugriff auf die Variable connection haben.
Problem
// Don’t do this
class Model {
insertInto(db) {
return new Promise((resolve, reject) => { // (A)
db.insert(this.fields)
.then(resultCode => {
this.notifyObservers({event: 'created', model: this});
resolve(resultCode);
}).catch(err => {
reject(err);
})
});
}
// ···
}In Zeile A erstellen wir ein Promise, um das Ergebnis von db.insert() zu liefern. Das ist unnötig umständlich und kann vereinfacht werden
class Model {
insertInto(db) {
return db.insert(this.fields)
.then(resultCode => {
this.notifyObservers({event: 'created', model: this});
return resultCode;
});
}
// ···
}Die Kernidee ist, dass wir kein Promise erstellen müssen; wir können das Ergebnis des .then()-Aufrufs zurückgeben. Ein zusätzlicher Vorteil ist, dass wir den Fehler von db.insert() nicht abfangen und erneut ablehnen müssen. Wir leiten seine Ablehnung einfach an den Aufrufer von .insertInto() weiter.
Sofern nicht anders angegeben, wurde die Funktionalität in ECMAScript 6 eingeführt (zu diesem Zeitpunkt wurden Promises zur Sprache hinzugefügt).
Glossar
Promise.all()Promise.all<T>(promises: Iterable<Promise<T>>)
: Promise<Array<T>>P: wenn alle Input-Promises erfüllt sind.P: wenn ein Input-Promise abgelehnt wird.Promise.race()Promise.race<T>(promises: Iterable<Promise<T>>)
: Promise<T>P: wenn das erste Input-Promise beendet wird.Promise.any() [ES2021]Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>P: wenn ein Input-Promise erfüllt wird.P: wenn alle Input-Promises abgelehnt werden.AggregateError, das die Ablehnungswerte der Input-Promises enthält.Dies ist die Typensignatur von AggregateError (einige Member wurden weggelassen)
class AggregateError {
constructor(errors: Iterable<any>, message: string);
get errors(): Array<any>;
get message(): string;
}Promise.allSettled() [ES2020]Promise.allSettled<T>(promises: Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>P: wenn alle Input-Promises beendet sind.P: wenn beim Iterieren über die Input-Promises ein Fehler auftritt.Dies ist die Typensignatur von SettlementObject
type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;
interface FulfillmentObject<T> {
status: 'fulfilled';
value: T;
}
interface RejectionObject {
status: 'rejected';
reason: unknown;
}