javascript beispiel - Wie wird eine Promise / Defer-Bibliothek implementiert?



then all (5)

Wie wird eine Promise / Defer-Bibliothek wie q implementiert? Ich habe versucht, den Quellcode zu lesen, aber ich fand es ziemlich schwer zu verstehen, also dachte ich mir, es wäre großartig, wenn mir jemand auf einer hohen Ebene erklären würde, welche Techniken verwendet werden, um Versprechen in JS-Umgebungen mit einem Thread zu implementieren wie Node und Browser.


Answers

Q ist in Bezug auf die Implementierung eine sehr komplexe Verspre- chensbibliothek, da es darauf abzielt, Pipelining- und RPC-Typ-Szenarien zu unterstützen. Ich habe meine eigene Implementierung der Promises/A+ Spezifikation here .

Im Prinzip ist es ganz einfach. Bevor das Versprechen erfüllt / gelöst wird, notieren Sie alle Callbacks oder Fehler, indem Sie sie in ein Array einfügen. Wenn das Versprechen erfüllt ist, rufen Sie die entsprechenden Rückrufe oder Rückbuchungen auf und zeichnen auf, mit welchem ​​Ergebnis das Versprechen abgewickelt wurde (und ob es erfüllt oder zurückgewiesen wurde). Nach der Bearbeitung rufen Sie die Callbacks oder Errbacks einfach mit dem gespeicherten Ergebnis auf.

Das gibt Ihnen ungefähr die Semantik von done . Um zu bauen, müssen Sie nur ein neues Versprechen zurückgeben, das mit dem Ergebnis des Aufrufs der Rückrufe / Fehler behoben ist.

Wenn Sie an einer vollständigen Erläuterung der Gründe für die Entwicklung einer Full-on-Promise-Implementierung mit Unterstützung für RPC und Pipelining wie Q interessiert sind, können Sie here Hinweis von kriskowal lesen. Es ist ein wirklich netter abgestufter Ansatz, den ich nicht genug empfehlen kann, wenn Sie daran denken, Versprechungen zu implementieren. Es ist wahrscheinlich eine Lektüre wert, auch wenn Sie nur eine Versprechens-Bibliothek verwenden werden.


Vielleicht möchten Sie den Blog-Post auf Adehun überprüfen.

Adehun ist eine extrem leichte Implementierung (etwa 166 LOC) und sehr nützlich, um zu lernen, wie man die Promise / A + Spezifikation implementiert.

Haftungsausschluss : Ich habe den Blogbeitrag geschrieben, aber der Blogbeitrag erklärt alles über Adehun.

Die Übergangsfunktion - Gatekeeper für den Zustandsübergang

Gatekeeper-Funktion; stellt sicher, dass Zustandsübergänge auftreten, wenn alle erforderlichen Bedingungen erfüllt sind.

Wenn Bedingungen erfüllt sind, aktualisiert diese Funktion den Status und Wert des Versprechens. Es löst dann die Prozessfunktion zur weiteren Verarbeitung aus.

Die Prozessfunktion führt die richtige Aktion basierend auf dem Übergang aus (z. B. ausstehend bis erfüllt) und wird später erläutert.

function transition (state, value) {
  if (this.state === state ||
    this.state !== validStates.PENDING ||
    !isValidState(state)) {
      return;
    }

  this.value = value;
  this.state = state;
  this.process();
}

Die Dann-Funktion

Die then-Funktion akzeptiert zwei optionale Argumente (onFulfill und onReject-Handler) und muss ein neues Versprechen zurückgeben. Zwei Hauptanforderungen:

  1. Das Basisversprechen (dasjenige, auf dem dann aufgerufen wird) muss ein neues Versprechen unter Verwendung der übergebenen Handler erstellen; Die Basis speichert außerdem eine interne Referenz auf diese erstellte Verheißung, sodass sie aufgerufen werden kann, sobald die Basisversprechung erfüllt / abgelehnt wurde.

  2. Wenn das Basisversprechen erfüllt ist (dh erfüllt oder abgelehnt), sollte der entsprechende Handler sofort aufgerufen werden. Adehun.js behandelt dieses Szenario, indem es in der then-Funktion den Prozess aufruft.

""

function then(onFulfilled, onRejected) {
    var queuedPromise = new Adehun();
    if (Utils.isFunction(onFulfilled)) {
        queuedPromise.handlers.fulfill = onFulfilled;
    }

    if (Utils.isFunction(onRejected)) {
        queuedPromise.handlers.reject = onRejected;
    }

    this.queue.push(queuedPromise);
    this.process();

    return queuedPromise;
}`

Die Funktion Prozess - Übergänge verarbeiten

Dies wird nach Zustandsübergängen oder wenn die then-Funktion aufgerufen wird aufgerufen. Daher muss es nach ausstehenden Versprechungen suchen, da es möglicherweise von der then-Funktion aufgerufen wurde.

Process führt das Promise Resolution-Verfahren für alle intern gespeicherten Versprechungen aus (dh für diejenigen, die über die then-Funktion mit dem Basisversprechen verknüpft wurden) und erzwingt die folgenden Anforderungen von Promise / A +:

  1. Asynchroner Aufruf der Handler mit dem Hilfsprogramm Utils.runAsync (ein dünner Wrapper um setTimeout (setImmediate funktioniert auch)).

  2. Fallback-Handler für die onSuccess- und onReject-Handler erstellen, wenn sie fehlen.

  3. Auswahl der richtigen Handler-Funktion basierend auf dem Zusage-Status, zB erfüllt oder abgelehnt.

  4. Anwenden des Handlers auf den Wert der Basisversprechung Der Wert dieser Operation wird an die Resolve-Funktion übergeben, um den Versprechungsverarbeitungszyklus abzuschließen.

  5. Wenn ein Fehler auftritt, wird das angehängte Versprechen sofort abgelehnt.

    function process () {var that = Dies, fulfillFallBack = function (value) {Rückgabewert; }, rejectFallBack = function (Grund) {throw reason; };

    if (this.state === validStates.PENDING) {
        return;
    }
    
    Utils.runAsync(function() {
        while (that.queue.length) {
            var queuedP = that.queue.shift(),
                handler = null,
                value;
    
            if (that.state === validStates.FULFILLED) {
                handler = queuedP.handlers.fulfill ||
                    fulfillFallBack;
            }
            if (that.state === validStates.REJECTED) {
                handler = queuedP.handlers.reject ||
                    rejectFallBack;
            }
    
            try {
                value = handler(that.value);
            } catch (e) {
                queuedP.reject(e);
                continue;
            }
    
            Resolve(queuedP, value);
        }
    });
    

    }

Die Resolve-Funktion - Auflösen von Versprechen

Dies ist wahrscheinlich der wichtigste Teil der Versprechen Implementierung, da es Versprechen Auflösung behandelt. Er akzeptiert zwei Parameter - das Versprechen und seinen Auflösungswert.

Es gibt viele Überprüfungen für verschiedene mögliche Auflösungswerte; die interessanten Auflösungsszenarien sind zwei - solche, bei denen ein Versprechen übergeben wird, und ein Dannable (ein Objekt mit einem dann-Wert).

  1. Einen Promise-Wert übergeben

Wenn der Auflösungswert ein anderes Versprechen ist, muss das Versprechen den Status dieses Auflösungswerts annehmen. Da dieser Auflösungswert ausstehend oder festgelegt sein kann, besteht der einfachste Weg darin, einen neuen then-Handler an den Auflösungswert anzuhängen und das ursprüngliche Versprechen darin zu behandeln. Wann immer es sich niederschlägt, wird das ursprüngliche Versprechen gelöst oder abgelehnt.

  1. Einen anrechenbaren Wert übergeben

Der Haken hierbei ist, dass die thenable-Funktion des thenable-Werts nur einmal aufgerufen werden muss (eine gute Verwendung für den einmaligen Wrapper aus der funktionalen Programmierung). Wenn das Abrufen der then-Funktion eine Ausnahme auslöst, wird das Versprechen sofort zurückgewiesen.

Wie zuvor wird die then-Funktion mit Funktionen aufgerufen, die das Versprechen letztendlich auflösen oder zurückweisen, aber der Unterschied ist hier das angerufene Flag, das beim ersten Aufruf gesetzt wird, und nachfolgende Aufrufe sind keine Ops.

function Resolve(promise, x) {
  if (promise === x) {
    var msg = "Promise can't be value";
    promise.reject(new TypeError(msg));
  }
  else if (Utils.isPromise(x)) {
    if (x.state === validStates.PENDING){
      x.then(function (val) {
        Resolve(promise, val);
      }, function (reason) {
        promise.reject(reason);
      });
    } else {
      promise.transition(x.state, x.value);
    }
  }
  else if (Utils.isObject(x) ||
           Utils.isFunction(x)) {
    var called = false,
        thenHandler;

    try {
      thenHandler = x.then;

      if (Utils.isFunction(thenHandler)){
        thenHandler.call(x,
          function (y) {
            if (!called) {
              Resolve(promise, y);
              called = true;
            }
          }, function (r) {
            if (!called) {
              promise.reject(r);
              called = true;
            }
       });
     } else {
       promise.fulfill(x);
       called = true;
     }
   } catch (e) {
     if (!called) {
       promise.reject(e);
       called = true;
     }
   }
 }
 else {
   promise.fulfill(x);
 }
}

Der Promise-Konstruktor

Und das ist alles, was alles zusammenbringt. Die Funktionen erfüllen und ablehnen sind syntaktische Zucker, die No-Op-Funktionen zur Auflösung und Zurückweisung übergeben.

var Adehun = function (fn) {
 var that = this;

 this.value = null;
 this.state = validStates.PENDING;
 this.queue = [];
 this.handlers = {
   fulfill : null,
   reject : null
 };

 if (fn) {
   fn(function (value) {
     Resolve(that, value);
   }, function (reason) {
     that.reject(reason);
   });
 }
};

Ich hoffe, dies hat dazu beigetragen, mehr Licht in die Art und Weise zu bringen, wie Versprechungen funktionieren.


Wie Forbes in seiner Antwort erwähnt, habe ich viele der Designentscheidungen aufgezeichnet, die mit der Erstellung einer Bibliothek wie Q verbunden sind, hier https://github.com/kriskowal/q/tree/v1/design . Es genügt zu sagen, dass es Ebenen einer Versprechens-Bibliothek gibt, und viele Bibliotheken, die auf verschiedenen Ebenen anhalten.

Auf der ersten Ebene, die von der Promises / A + -Spezifikation erfasst wird, ist ein Versprechen ein Proxy für ein eventuelles Ergebnis und eignet sich zum Verwalten von "lokaler Asynchronität" . Das heißt, es ist geeignet, um sicherzustellen, dass die Arbeit in der richtigen Reihenfolge stattfindet, und um sicherzustellen, dass es einfach und unkompliziert ist, auf das Ergebnis einer Operation zu hören, unabhängig davon, ob sie bereits erledigt ist oder in Zukunft auftreten wird. Es macht es ebenso einfach für eine oder mehrere Parteien, ein eventuelles Ergebnis zu abonnieren.

Q, wie ich es implementiert habe, verspricht Versprechungen, die Proxies für eventuelle Fern- oder Event-Fern-Ergebnisse sind. Zu diesem Zweck ist das Design invertiert, mit verschiedenen Implementierungen für Versprechen - aufgeschobene Versprechen, erfüllte Versprechen, abgelehnte Versprechen und Versprechen für entfernte Objekte (das letzte wird in Q-Connection implementiert). Sie alle teilen die gleiche Schnittstelle und arbeiten, indem sie Nachrichten wie "dann" senden und empfangen (was für Promises / A + ausreicht), aber auch "get" und "invoke". Q steht also für "verteilte Asynchronität" und existiert auf einer anderen Ebene.

Allerdings wurde Q tatsächlich von einer höheren Ebene entfernt, wo Versprechungen für die Verwaltung verteilter Asynchronität zwischen gegenseitig verdächtigen Parteien wie Ihnen, einem Händler, einer Bank, Facebook, der Regierung - nicht Feinde, vielleicht sogar Freunden, aber manchmal mit Konflikten - verwendet werden Interesse. Das Q, das ich implementiert habe, ist API-kompatibel mit gehärteten Sicherheitsversprechen (was der Grund dafür ist, promise und resolve zu trennen), in der Hoffnung, dass es Leute zu Versprechen führt, sie in der Verwendung dieser API schult und ihnen erlaubt ihren Code mit ihnen, wenn sie Versprechen in sicheren Mashups in der Zukunft verwenden müssen.

Natürlich gibt es Kompromisse, wenn Sie die Ebenen nach oben bewegen, normalerweise in der Geschwindigkeit. Also, Versprechen verspricht Implementierungen können auch koexistieren. Hier tritt das Konzept eines "ténable" ein. Promise-Bibliotheken auf jeder Ebene können so gestaltet werden, dass sie Versprechungen von jeder anderen Ebene konsumieren, sodass mehrere Implementierungen koexistieren können und Nutzer nur kaufen können, was sie benötigen.

Alles in allem gibt es keine Entschuldigung dafür, schwer lesbar zu sein. Domenic und ich arbeiten an einer Version von Q, die modularer und zugänglicher ist, mit einigen ihrer störenden Abhängigkeiten und Workarounds in andere Module und Pakete verschoben wurde. Zum Glück haben Leute wie here , Crockford und andere die Bildungslücke mit einfacheren Bibliotheken gefüllt.


Stellen Sie zuerst sicher, dass Sie verstehen, wie Promises funktionieren sollen. Werfen Sie einen Blick auf die Vorschläge von CommonJs Promises und die Spezifikation von Promises / A + .

Es gibt zwei grundlegende Konzepte, die jeweils in ein paar einfachen Zeilen implementiert werden können:

  • Ein Versprechen wird asynchron mit dem Ergebnis aufgelöst. Das Hinzufügen von Callbacks ist eine transparente Aktion - unabhängig davon, ob das Versprechen bereits gelöst wurde oder nicht, werden sie mit dem Ergebnis aufgerufen, sobald es verfügbar ist.

    function Deferred() {
        var callbacks = [], // list of callbacks
            result; // the resolve arguments or undefined until they're available
        this.resolve = function() {
            if (result) return; // if already settled, abort
            result = arguments; // settle the result
            for (var c;c=callbacks.shift();) // execute stored callbacks
                c.apply(null, result);
        });
        // create Promise interface with a function to add callbacks:
        this.promise = new Promise(function add(c) {
            if (result) // when results are available
                c.apply(null, result); // call it immediately
            else
                callbacks.push(c); // put it on the list to be executed later
        });
    }
    // just an interface for inheritance
    function Promise(add) {
        this.addCallback = add;
    }
    
  • Versprechen haben eine then Methode, die es erlaubt, sie zu verketten. Ich nehme einen Rückruf und gebe ein neues Versprechen zurück, das mit dem Ergebnis dieses Rückrufs aufgelöst wird, nachdem es mit dem Ergebnis des ersten Versprechens aufgerufen wurde. Wenn der Rückruf ein Versprechen zurückgibt, wird er assimiliert, anstatt geschachtelt zu werden.

    Promise.prototype.then = function(fn) {
        var dfd = new Deferred(); // create a new result Deferred
        this.addCallback(function() { // when `this` resolves…
            // execute the callback with the results
            var result = fn.apply(null, arguments);
            // check whether it returned a promise
            if (result instanceof Promise)
                result.addCallback(dfd.resolve); // then hook the resolution on it
            else
                dfd.resolve(result); // resolve the new promise immediately 
            });
        });
        // and return the new Promise
        return dfd.promise;
    };
    

Weitere Konzepte würden darin bestehen, einen separaten Fehlerzustand (mit einem zusätzlichen Rückruf dafür) aufrechtzuerhalten und Ausnahmen in den Handlern abzufangen oder Asynchronität für die Rückrufe zu garantieren. Sobald Sie diese hinzugefügt haben, haben Sie eine voll funktionsfähige Promise-Implementierung.

Hier ist die Fehlersache geschrieben. Es ist leider ziemlich repetitiv; Sie können es besser machen, indem Sie zusätzliche Verschlüsse verwenden, aber dann wird es wirklich sehr schwer zu verstehen.

function Deferred() {
    var callbacks = [], // list of callbacks
        errbacks = [], // list of errbacks
        value, // the fulfill arguments or undefined until they're available
        reason; // the error arguments or undefined until they're available
    this.fulfill = function() {
        if (reason || value) return false; // can't change state
        value = arguments; // settle the result
        for (var c;c=callbacks.shift();)
            c.apply(null, value);
        errbacks.length = 0; // clear stored errbacks
    });
    this.reject = function() {
        if (value || reason) return false; // can't change state
        reason = arguments; // settle the errror
        for (var c;c=errbacks.shift();)
            c.apply(null, reason);
        callbacks.length = 0; // clear stored callbacks
    });
    this.promise = new Promise(function add(c) {
        if (reason) return; // nothing to do
        if (value)
            c.apply(null, value);
        else
            callbacks.push(c);
    }, function add(c) {
        if (value) return; // nothing to do
        if (reason)
            c.apply(null, reason);
        else
            errbacks.push(c);
    });
}
function Promise(addC, addE) {
    this.addCallback = addC;
    this.addErrback = addE;
}
Promise.prototype.then = function(fn, err) {
    var dfd = new Deferred();
    this.addCallback(function() { // when `this` is fulfilled…
        try {
            var result = fn.apply(null, arguments);
            if (result instanceof Promise) {
                result.addCallback(dfd.fulfill);
                result.addErrback(dfd.reject);
            } else
                dfd.fulfill(result);
        } catch(e) { // when an exception was thrown
            dfd.reject(e);
        }
    });
    this.addErrback(err ? function() { // when `this` is rejected…
        try {
            var result = err.apply(null, arguments);
            if (result instanceof Promise) {
                result.addCallback(dfd.fulfill);
                result.addErrback(dfd.reject);
            } else
                dfd.fulfill(result);
        } catch(e) { // when an exception was re-thrown
            dfd.reject(e);
        }
    } : dfd.reject); // when no `err` handler is passed then just propagate
    return dfd.promise;
};

Natürlich gibt es viele Ansätze wie synchrones Anfordern, Versprechen, aber aus meiner Erfahrung sollten Sie den Callback-Ansatz verwenden. Es ist natürlich für asynchrones Verhalten von Javascript. Ihr Code-Snippet kann also etwas anders geschrieben werden:

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            myCallback(response);
        }
    });

    return result;
}

function myCallback(response) {
    // Does something.
}




javascript promise