Erweitern Sie das Javascript-Versprechen und lösen Sie es auf oder lehnen Sie es im Konstruktor ab

Lesezeit: 8 Minuten

Benutzer-Avatar
humkins

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:

  1. Unterklassenkonstruktor wird zweimal ausgeführt
  2. 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)
        })

Benutzer-Avatar
Traktor

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 thenzu 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);
}

  • Danke @traktor53 für die vollständige Logikbeschreibung. Etwas wie jsfiddle.net/p7b6gaqd/15 sollte auch gehen denke ich?

    – Seelenmensch

    28. November 2018 um 12:58 Uhr


  • @Soul_man der Code scheint in die richtige Richtung zu gehen, liegt aber wie erwähnt “außerhalb des Geltungsbereichs dieser Antwort”. Die gegebenen Kommentare sind nicht der Ort, um bestehende Fragen zu erweitern, bitte stellen Sie eine neue Frage, entweder hier oder weiter Code-Review wenn Sie zusätzliche Unterstützung und/oder Feedback wünschen. Es gibt auch anderen die Chance zu antworten 🙂

    – Traktor

    29. November 2018 um 0:06 Uhr


  • Also, weil die MyPromiseKonstruktor, nicht Promise‘s, wird verwendet, um zu konstruieren abgeleitet Promises, genauso Promise‘s würde ausreichen, Sie müssen den angegebenen Executor (falls vorhanden) ausführen und ihn richtig füttern resolve und reject Funktionen aus der Superklasse, Promisein dem MyPromiseDer Konstruktor von . Okay, ich glaube, ich habe es verstanden.

    – Константин Ван

    21. April 2020 um 22:48 Uhr


Benutzer-Avatar
asdru

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)
    })

  • Ich habe einen ähnlichen Ansatz für meine verwendet CancellablePromise aber das war mir nicht bewusst [theSymbol.species] Trick, danke dafür!

    – Noseration

    18. Juli 2020 um 23:46 Uhr

  • Außerdem: Der Konstruktor einer benutzerdefinierten Promise-Klasse wird zweimal aufgerufen (erweitert das Standard-Promise).

    – Noseration

    12. August 2020 um 3:10 Uhr

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 traktorAntwortanalysen 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, …).

  • Aber wenn Sie die Kompatibilität mit dem nicht beibehalten Promise Konstruktor, den Sie nicht verwenden können MyPromise.race und MyPromise.all, wodurch das LSP-SOLID-Prinzip gebrochen wird. für die Symbol.toStringTagja ist ziemlich nutzlos, ich habe es nur der Vollständigkeit halber hinzugefügt

    – asdru

    30. März 2021 um 14:56 Uhr

  • Rückkehr Promise von dem Symbol.species getter verursacht Aufrufe an die then Methode von MyPromise-Objekten, um stattdessen ein Promise-Objekt zurückzugeben a MyPromise Objekt, wodurch die Erweiterung bestenfalls partiell wird. Wenn Sie den Symbol.species-Getter weglassen, werden Aufrufe an die geerbte then -Methode von Mypromise-Objekten gibt einen Fehler aus, da der „erweiterte“ Klassenkonstruktor keine Executor-Funktion unterstützt (wie im Beitrag beschrieben).

    – Traktor

    13. September 2021 um 0:15 Uhr

Du musst es schaffen thenfähig durch die Umsetzung der then Methode.

Ansonsten die der Oberklasse, Promisewird 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.

1015170cookie-checkErweitern Sie das Javascript-Versprechen und lösen Sie es auf oder lehnen Sie es im Konstruktor ab

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy