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




javascript promise then (4)

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.

https://code.i-harness.com


Ich finde es schwieriger zu erklären, als ein Beispiel zu zeigen, deshalb ist hier eine sehr einfache Implementierung dessen, was ein Aufschub / Versprechen sein könnte.

Haftungsausschluss: Dies ist keine funktionale Implementierung und einige Teile der Promise / A-Spezifikation fehlen, Dies ist nur um die Grundlage der Versprechen zu erklären.

tl; dr: Gehen Sie zum Abschnitt Klassen und Beispiele erstellen , um die vollständige Implementierung anzuzeigen.

Versprechen:

Zuerst müssen wir ein Versprechen-Objekt mit einem Array von Callbacks erstellen. Ich werde anfangen mit Objekten zu arbeiten, weil es klarer ist:

var promise = {
  callbacks: []
}

Fügen Sie nun Callbacks mit der Methode hinzu:

var promise = {
  callbacks: [],
  then: function (callback) {
    callbacks.push(callback);
  }
}

Und wir brauchen auch die Fehlerrückrufe:

var promise = {
  okCallbacks: [],
  koCallbacks: [],
  then: function (okCallback, koCallback) {
    okCallbacks.push(okCallback);
    if (koCallback) {
      koCallbacks.push(koCallback);
    }
  }
}

Verschieben:

Erstellen Sie nun das Verzögerungsobjekt, das eine Versprechung haben soll:

var defer = {
  promise: promise
};

Die Verschiebung muss gelöst werden:

var defer = {
  promise: promise,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },
};

Und muss ablehnen:

var defer = {
  promise: promise,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },

  reject: function (error) {
    this.promise.koCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(error)
      }, 0);
    });
  }
};

Beachten Sie, dass die Callbacks in einer Zeitüberschreitung aufgerufen werden, damit der Code immer asynchron ist.

Und das ist es, was eine grundlegende Verzögerung / Versprechen Implementierung benötigt.

Erstellen Sie Klassen und Beispiel:

Jetzt können wir beide Objekte in Klassen konvertieren, zuerst das Versprechen:

var Promise = function () {
  this.okCallbacks = [];
  this.koCallbacks = [];
};

Promise.prototype = {
  okCallbacks: null,
  koCallbacks: null,
  then: function (okCallback, koCallback) {
    okCallbacks.push(okCallback);
    if (koCallback) {
      koCallbacks.push(koCallback);
    }
  }
};

Und jetzt der Aufschub:

var Defer = function () {
  this.promise = new Promise();
};

Defer.prototype = {
  promise: null,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },

  reject: function (error) {
    this.promise.koCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(error)
      }, 0);
    });
  }
};

Und hier ist ein Anwendungsbeispiel:

function test() {
  var defer = new Defer();
  // an example of an async call
  serverCall(function (request) {
    if (request.status === 200) {
      defer.resolve(request.responseText);
    } else {
      defer.reject(new Error("Status code was " + request.status));
    }
  });
  return defer.promise;
}

test().then(function (text) {
  alert(text);
}, function (error) {
  alert(error.message);
});

Wie Sie sehen können, sind die grundlegenden Teile einfach und klein. Es wird größer, wenn Sie andere Optionen hinzufügen, z. B. die Auflösung mehrerer Versprechen:

Defer.all(promiseA, promiseB, promiseC).then()

oder Versprechen Verkettung:

getUserById(id).then(getFilesByUser).then(deleteFile).then(promptResult);

Um mehr über die Spezifikationen zu lesen: CommonJS Promise Specification . Beachten Sie, dass die Hauptbibliotheken (Q, when.js, rsvp.js, node-promise, ...) der Promises/A Spezifikation folgen.

Hoffe ich war klar genug.

Bearbeiten:

Wie in den Kommentaren gefragt, habe ich zwei Dinge in dieser Version hinzugefügt:

  • Die Möglichkeit, dann von einem Versprechen zu rufen, egal welchen Status es hat.
  • Die Möglichkeit, Versprechen zu verketten.

Um das Versprechen bei der Auflösung aufrufen zu können, müssen Sie dem Versprechen den Status hinzufügen, und wenn der dann aufgerufen wird, überprüfen Sie diesen Status. Wenn der Status aufgelöst oder zurückgewiesen wird, führen Sie den Rückruf mit seinen Daten oder Fehlern aus.

Um Versprechen verketten zu können, müssen Sie für jeden Anruf eine neue Verzögerung generieren und, wenn das Versprechen aufgelöst / abgelehnt wird, das neue Versprechen mit dem Ergebnis des Rückrufs auflösen / ablehnen. Wenn also das Versprechen abgeschlossen ist und der Rückruf ein neues Versprechen zurückgibt, ist er an das mit then() Versprechen gebunden. Wenn nicht, wird das Versprechen mit dem Ergebnis des Rückrufs aufgelöst.

Hier ist das Versprechen:

var Promise = function () {
  this.okCallbacks = [];
  this.koCallbacks = [];
};

Promise.prototype = {
  okCallbacks: null,
  koCallbacks: null,
  status: 'pending',
  error: null,

  then: function (okCallback, koCallback) {
    var defer = new Defer();

    // Add callbacks to the arrays with the defer binded to these callbacks
    this.okCallbacks.push({
      func: okCallback,
      defer: defer
    });

    if (koCallback) {
      this.koCallbacks.push({
        func: koCallback,
        defer: defer
      });
    }

    // Check if the promise is not pending. If not call the callback
    if (this.status === 'resolved') {
      this.executeCallback({
        func: okCallback,
        defer: defer
      }, this.data)
    } else if(this.status === 'rejected') {
      this.executeCallback({
        func: koCallback,
        defer: defer
      }, this.error)
    }

    return defer.promise;
  },

  executeCallback: function (callbackData, result) {
    window.setTimeout(function () {
      var res = callbackData.func(result);
      if (res instanceof Promise) {
        callbackData.defer.bind(res);
      } else {
        callbackData.defer.resolve(res);
      }
    }, 0);
  }
};

Und der Aufschub:

var Defer = function () {
  this.promise = new Promise();
};

Defer.prototype = {
  promise: null,
  resolve: function (data) {
    var promise = this.promise;
    promise.data = data;
    promise.status = 'resolved';
    promise.okCallbacks.forEach(function(callbackData) {
      promise.executeCallback(callbackData, data);
    });
  },

  reject: function (error) {
    var promise = this.promise;
    promise.error = error;
    promise.status = 'rejected';
    promise.koCallbacks.forEach(function(callbackData) {
      promise.executeCallback(callbackData, error);
    });
  },

  // Make this promise behave like another promise:
  // When the other promise is resolved/rejected this is also resolved/rejected
  // with the same data
  bind: function (promise) {
    var that = this;
    promise.then(function (res) {
      that.resolve(res);
    }, function (err) {
      that.reject(err);
    })
  }
};

Wie Sie sehen können, ist es ziemlich gewachsen.


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.





promise