Ich möchte die native Javascript Promise-Klasse mit ES6-Syntax erweitern und eine asynchrone Funktion innerhalb des Unterklassenkonstruktors aufrufen können. Basierend auf dem Ergebnis der asynchronen Funktion muss das Promise entweder abgelehnt oder aufgelöst werden.
Allerdings passieren zwei seltsame Dinge, wenn then
Funktion heißt:
- Unterklassenkonstruktor wird zweimal ausgeführt
- Der Fehler „Uncaught TypeError: Promise-Auflösungs- oder Ablehnungsfunktion ist nicht aufrufbar“ wird ausgegeben
class MyPromise extends Promise {
constructor(name) {
super((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
this.name = name
}
}
new MyPromise('p1')
.then(result => {
console.log('resolved, result: ', result)
})
.catch(err => {
console.error('err: ', err)
})
Die Begründung ist einfach, aber nicht unbedingt selbstverständlich.
.then()
gibt ein Versprechen zurück
- wenn
then
auf einer Unterklasse von Promise aufgerufen wird, ist das zurückgegebene Promise eine Instanz der Unterklasse, nicht Promise selbst.
- das
then
Das zurückgegebene Versprechen wird konstruiert, indem der Unterklassenkonstruktor aufgerufen und an eine interne Ausführungsfunktion übergeben wird, die den Wert von aufzeichnet resolve
und reject
Argumente, die ihm zur späteren Verwendung übergeben werden.
- “spätere Verwendung” umfasst das Auflösen oder Zurückweisen des von zurückgegebenen Versprechens
then
asynchron beim Überwachen der Ausführung von onfulfilled
oder onrejected
Handler (später), um zu sehen, ob sie einen Wert zurückgeben (der die then
zurückgegebenes Versprechen) oder einen Fehler ausgeben (der das Versprechen ablehnt).
Zusamenfassend then
Aufrufe erhalten intern Verweise auf die und zeichnen sie auf resolve
und reject
Funktionen von Versprechen, die sie zurückgeben.
Also zu der Frage,
new MyPromise( 'p1')
funktioniert einwandfrei und ist der erste Aufruf des Unterklassenkonstruktors.
.then( someFunction)
Aufzeichnungen someFunction
in einer Liste von then
getätigte Anrufe new MyPromise
(abrufen then
mehrfach aufrufbar) und versucht, durch Aufruf eine Rückholzusage zu erstellen
new MyPromise( (resolve, reject) => ... /* store resolve reject references */
Dies ist der zweite Aufruf an den Unterklassenkonstruktor, der von kommt then
Code. Es wird erwartet, dass der Konstruktor synchron zurückkehrt (und dies auch tut).
Bei der Rückkehr vom Erstellen des Rückkehrversprechens, das .then
-Methode führt eine Integritätsprüfung durch, um festzustellen, ob die resolve
und reject
Funktionen, die es später benötigt, sind eigentlich Funktionen. Sie sollten (in einer Liste) zusammen mit den in bereitgestellten Rückrufen gespeichert worden sein then
Anruf.
Im Falle des MyPromise
Sie sind nicht. Der Vollstrecker ging vorbei then
zu MyPromise
, wird nicht einmal genannt. So then
Der Methodencode gibt einen Typfehler „Funktion zum Auflösen oder Zurückweisen des Versprechens ist nicht aufrufbar“ aus – er hat keine Möglichkeit, das Versprechen aufzulösen oder abzulehnen, das er zurückgeben soll.
Beim Erstellen einer Unterklasse von Promise muss der Unterklassenkonstruktor eine Executor-Funktion als erstes Argument nehmen und den Executor mit Real aufrufen resolve
und reject
funktionale Argumente. Dies wird intern von benötigt then
Methodencode.
Etwas Kompliziertes tun mit MyPromise
, möglicherweise den ersten Parameter zu überprüfen, um zu sehen, ob es sich um eine Funktion handelt, und ihn als Executor aufzurufen, wenn dies der Fall ist, ist möglicherweise machbar, liegt jedoch außerhalb des Bereichs dieser Antwort! Für den gezeigten Code kann das Schreiben einer Fabrik-/Bibliotheksfunktion einfacher sein:
function namedDelay(name, delay=1000, value=1) {
var promise = new Promise( (resolve,reject) => {
setTimeout(() => {
resolve(value)
}, delay)
}
);
promise.name = name;
return promise;
}
namedDelay( 'p1')
.then(result => {
console.log('fulfilled, result: ', result)
})
.catch(err => {
console.error('err: ', err)
})
;TLDR
Die Klassenerweiterung für Promise ist keine Erweiterung. Wenn dies der Fall wäre, müsste die Promise-Schnittstelle implementiert und eine Executor-Funktion als erster Parameter verwendet werden. Sie könnten eine Factory-Funktion verwenden, um ein Promise zurückzugeben, das asynchron aufgelöst wird (wie oben), oder hacken den geposteten Code mit
MyPromise.prototype.constructor = Promise
was verursacht .then
um ein reguläres Promise-Objekt zurückzugeben. Der Hack selbst widerlegt die Idee, dass eine Klassenerweiterung stattfindet.
Beispiel für Promise-Erweiterung
Das folgende Beispiel zeigt eine grundlegende Promise-Erweiterung, die dem Konstruktor bereitgestellte Eigenschaften hinzufügt. Bemerkenswert:
-
Das Symbol.toString
Getter wirkt sich nur auf die Ausgabe der Konvertierung einer Instanz in einen String aus. Beim Protokollieren einer Instanz wird “Promise” nicht in “MyPromise” geändert Objekt auf Browserkonsolen getestet.
-
Firefox 89 (Proton) meldet keine eigenen Eigenschaften von erweiterten Instanzen, während Chrome dies tut – der Grund für den Testcode unten protokolliert Instanzeigenschaften nach Namen.
class MyPromise extends Promise {
constructor(exec, props) {
if( typeof exec != "function") {
throw TypeError( "new MyPromise(executor, props): an executor function is required");
}
super((resolve, reject) => exec(resolve,reject));
if( props) {
Object.assign( this, props);
}
}
get [Symbol.toStringTag]() {
return 'MyPromise';
}
}
// Test the extension:
const p1 = new MyPromise( (resolve, reject) =>
resolve(42),
{id: "p1", bark: ()=>console.log("woof") });
console.log( "p1 is a %s object", p1.constructor.name);
console.log( "p1.toString() = %s", p1.toString());
console.log( "p1.id = '%s'", p1.id);
console.log( "p1 says:"); p1.bark();
const pThen = p1.then(data=>data);
console.log( "p1.then() returns a %s object", pThen.constructor.name);
let pAll = MyPromise.all([Promise.resolve(39)]);
console.log( "MyPromise.all returns a %s object", pAll.constructor.name);
try { new MyPromise(); }
catch(err) {
console.log( "new MyPromise() threw: '%s'", err.message);
}
Der beste Weg, den ich gefunden habe, um ein Versprechen zu verlängern, ist
class MyPromise extends Promise {
constructor(name) {
// needed for MyPromise.race/all ecc
if(name instanceof Function){
return super(name)
}
super((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
this.name = name
}
// you can also use Symbol.species in order to
// return a Promise for then/catch/finally
static get [Symbol.species]() {
return Promise;
}
// Promise overrides his Symbol.toStringTag
get [Symbol.toStringTag]() {
return 'MyPromise';
}
}
new MyPromise('p1')
.then(result => {
console.log('resolved, result: ', result)
})
.catch(err => {
console.error('err: ', err)
})
Der Beitrag von asdru
enthält die richtige Antwort, enthält aber auch einen Ansatz (Konstruktor-Hack), von dem abgeraten werden sollte.
Der Konstruktor-Hack prüft, ob das Konstruktor-Argument eine Funktion ist. Das ist nicht der Weg zu gehen, da das ECMAScript-Design einen spezifischen Mechanismus zum Unterklassifizieren von Promises via enthält Symbol.species
.
asdru
‘s Kommentar zur Verwendung Symbol.species
ist richtig. Siehe die Erklärung in der aktuellen ECMAScript-Spezifikation:
Promise-Prototypmethoden verwenden normalerweise den Konstruktor dieses Werts, um ein abgeleitetes Objekt zu erstellen. Ein Unterklassenkonstruktor kann dieses Standardverhalten jedoch überschreiben, indem er seine Eigenschaft @@species neu definiert.
Auf diesen Hinweis verweist die Angabe (indirekt) in den Abschnitten auf finally
und then
(Suchen Sie nach Erwähnungen von SpeciesConstructor
).
Durch die Rückkehr Promise
als Artenkonstrukteur, die Probleme, die traktor
Antwortanalysen werden so deutlich vermieden. then
ruft die Promise
Konstruktor, aber nicht die Unterklasse MyPromise
Konstrukteur. Das MyPromise
Der Konstruktor wird nur einmal mit aufgerufen name
Argument und es ist keine weitere Argumentüberprüfungslogik erforderlich oder angemessen.
Daher sollte der Code einfach lauten:
class MyPromise extends Promise {
constructor(name) {
super((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
this.name = name
}
static get [Symbol.species]() {
return Promise;
}
get [Symbol.toStringTag]() {
return 'MyPromise';
}
}
Weniger ist mehr!
Einige Notizen:
-
MDN hat ein Beispiel für die Verwendung des Artensymbols in der Erweiterung Array
.
-
Die neuesten Browserversionen (Chrome, FF, Safari, Edge auf MAC und Linux) handhaben dies korrekt, aber ich habe keine Informationen zu anderen Browsern oder älteren Versionen.
-
Symbol.toStringTag
ist eine sehr nette Geste, aber nicht erforderlich. Die meisten Browser verwenden den für dieses Symbol zurückgegebenen Wert, um das untergeordnete Versprechen in der Konsole zu identifizieren, aber Vorsicht, FF tut dies nicht – dies könnte leicht verwirrend sein. In allen Browsern jedoch new MyPromise('mine').toString()
Erträge "[object MyPromise]"
.
-
All dies ist auch unproblematisch, wenn Sie in Typescript schreiben.
-
Wie noseratio
weist darauf hin, dass ein primärer Anwendungsfall für die Erweiterung von Promises das Wrapping von (alten) APIs ist, die Abbruch- oder Abbruchlogik unterstützen (FileReader, fetch, …).
Du musst es schaffen then
fähig durch die Umsetzung der then
Methode.
Ansonsten die der Oberklasse, Promise
wird aufgerufen und versucht, eine weitere zu erstellen Promise
mit Ihrer MyPromise
‘ Konstruktor, der nicht mit dem Original kompatibel ist Promise
Konstrukteur.
Die Sache ist, es ist schwierig, das richtig zu implementieren then
Methode, die genauso funktioniert Promise
‘s tut. Sie werden wahrscheinlich am Ende eine Instanz von haben Promise
als Mitglied, nicht als Oberklasse.
github.com/nodejs/node/issues/13678#issuecomment-326812117
– Arman Charan
8. Januar 2018 um 22:20 Uhr
Relevante Diskussion bei Extending a Promise in Javascript.
– Cacheus
8. Mai um 18:13 Uhr