web-dev-qa-db-fra.com

Comment une bibliothèque de promesse / report est-elle implémentée?

Comment une bibliothèque de promesse/report comme q est-elle implémentée? J'essayais de lire le code source mais je l'ai trouvé assez difficile à comprendre, donc j'ai pensé que ce serait génial si quelqu'un pouvait m'expliquer, à un haut niveau, quelles sont les techniques utilisées pour implémenter les promesses dans les environnements JS à un seul thread comme Node et navigateurs.

74
Derek Chiang

Je trouve qu'il est plus difficile d'expliquer que de montrer un exemple, alors voici une mise en œuvre très simple de ce que pourrait être un report/promesse.

Avis de non-responsabilité: Ce n'est pas une implémentation fonctionnelle et certaines parties de la spécification Promise/A sont manquantes, Ceci est juste pour expliquer la base des promesses.

tl; dr: Accédez à la section Créer des classes et exemple pour voir mise en œuvre complète.

Promettre:

Nous devons d'abord créer un objet de promesse avec un tableau de rappels. Je vais commencer à travailler avec des objets car c'est plus clair:

var promise = {
  callbacks: []
}

ajoutez maintenant des rappels avec la méthode puis:

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

Et nous avons également besoin des rappels d'erreur:

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

Reporter:

Créez maintenant l'objet defer qui aura une promesse:

var defer = {
  promise: promise
};

Le report doit être résolu:

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

Et doit rejeter:

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

Notez que les rappels sont appelés dans un délai d'expiration pour permettre au code d'être toujours asynchrone.

Et c'est ce dont une mise en œuvre de base de différer/promettre a besoin.

Créez des classes et un exemple:

Permet maintenant de convertir les deux objets en classes, d'abord la promesse:

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

Et maintenant le report:

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

Et voici un exemple d'utilisation:

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

Comme vous pouvez le voir, les pièces de base sont simples et petites. Il augmentera lorsque vous ajouterez d'autres options, par exemple la résolution de promesses multiples:

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

ou promettre l'enchaînement:

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

Pour en savoir plus sur les spécifications: CommonJS Promise Specification . Notez que les bibliothèques principales (Q, when.js, rsvp.js, node-promise, ...) suivent la spécification Promises/A .

J'espère que j'ai été assez clair.

Modifier:

Comme demandé dans les commentaires, j'ai ajouté deux choses dans cette version:

  • La possibilité d'appeler alors une promesse, quel que soit son statut.
  • La possibilité d'enchaîner les promesses.

Pour pouvoir appeler la promesse une fois résolue, vous devez ajouter le statut à la promesse et lorsque le alors est appelé, vérifiez ce statut. Si le statut est résolu ou rejeté, exécutez simplement le rappel avec ses données ou erreur.

Pour pouvoir chaîner des promesses, vous devez générer un nouveau report pour chaque appel à then et, lorsque la promesse est résolue/rejetée, résoudre/rejeter la nouvelle promesse avec le résultat du rappel. Ainsi, lorsque la promesse est terminée, si le rappel renvoie une nouvelle promesse, il est lié à la promesse renvoyée avec la then(). Sinon, la promesse est résolue avec le résultat du rappel.

Voici la promesse:

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

Et le report:

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

Comme vous pouvez le voir, il a beaucoup augmenté.

148
Kaizo

Q est une bibliothèque de promesses très complexe en termes d'implémentation car elle vise à prendre en charge les scénarios de type pipeline et RPC. J'ai ma propre implémentation très simple de la spécification Promises/A +ici .

En principe, c'est assez simple. Avant que la promesse ne soit réglée/résolue, vous conservez un enregistrement de tous les rappels ou rappels en les poussant dans un tableau. Lorsque la promesse est réglée, vous appelez les rappels ou les rappels appropriés et enregistrez le résultat avec lequel la promesse a été réglée (et si elle a été remplie ou rejetée). Une fois qu'il est réglé, vous appelez simplement les rappels ou les rappels avec le résultat stocké.

Cela vous donne approximativement la sémantique de done. Pour construire then, il vous suffit de renvoyer une nouvelle promesse qui est résolue avec le résultat de l'appel des rappels/errbacks.

Si vous êtes intéressé par une explication complète du raisonnement derrière le développement d'une implémentation complète sur promesse avec le support de RPC et de pipelining comme Q, vous pouvez lire le raisonnement de kriskowal ici . C'est une approche graduée vraiment sympa que je ne saurais trop recommander si vous songez à mettre en œuvre des promesses. Cela vaut probablement la peine d'être lu même si vous allez simplement utiliser une bibliothèque de promesses.

7
ForbesLindesay

Comme Forbes le mentionne dans sa réponse, j'ai fait la chronique de nombreuses décisions de conception impliquées dans la création d'une bibliothèque comme Q, ici https://github.com/kriskowal/q/tree/v1/design . Il suffit de dire qu'il existe des niveaux de bibliothèque de promesses et de nombreuses bibliothèques qui s'arrêtent à différents niveaux.

Au premier niveau, capturé par la spécification Promises/A +, une promesse est un proxy pour un résultat éventuel et convient pour gérer "l'asynchronie locale" . Autrement dit, il convient de garantir que le travail se déroule dans le bon ordre et de s'assurer qu'il est simple et direct d'écouter le résultat d'une opération, qu'elle soit déjà réglée ou se produise à l'avenir. Cela permet également à une ou plusieurs parties de souscrire à un résultat éventuel.

Q, comme je l'ai implémenté, fournit des promesses qui sont des proxy pour des résultats éventuels, distants ou éventuels + distants. À cette fin, sa conception est inversée, avec différentes implémentations pour les promesses: promesses différées, promesses remplies, promesses rejetées et promesses pour les objets distants (la dernière étant implémentée dans Q-Connection). Ils partagent tous la même interface et fonctionnent en envoyant et en recevant des messages comme "alors" (ce qui est suffisant pour Promises/A +) mais aussi "get" et "invoke". Donc, Q est sur "asynchronie distribuée" , et existe sur une autre couche.

Cependant, Q a en fait été retiré d'une couche supérieure, où les promesses sont utilisées pour gérer l'asynchronie distribuée entre parties suspectes comme vous, un commerçant, une banque , Facebook, le gouvernement - pas des ennemis, peut-être même des amis, mais parfois des conflits d'intérêts. Le Q que j'ai implémenté est conçu pour être compatible API avec des promesses de sécurité renforcées (ce qui est la raison de séparer promise et resolve), avec l'espoir qu'il introduira les gens aux promesses, les formera en utilisant cette API, et leur permettre de prendre leur code avec eux s'ils ont besoin d'utiliser des promesses dans des mashups sécurisés à l'avenir.

Bien sûr, il y a des compromis lorsque vous montez les couches, généralement en vitesse. Ainsi, les implémentations promises peuvent également être conçues pour coexister. C'est là que le concept d'un "thématique" entre. Les bibliothèques de promesses de chaque couche peuvent être conçues pour consommer les promesses de toute autre couche, de sorte que plusieurs implémentations peuvent coexister et que les utilisateurs ne peuvent acheter que ce dont ils ont besoin.

Cela dit, il n'y a aucune excuse pour être difficile à lire. Domenic et moi travaillons sur une version de Q qui sera plus modulaire et accessible, avec certaines de ses dépendances et solutions de rechange distrayantes déplacées dans d'autres modules et packages. Heureusement, des gens comme Forbes , Crockford , et d'autres ont comblé le vide éducatif en créant des bibliothèques plus simples.

6
Kris Kowal

Assurez-vous d'abord de comprendre comment les promesses sont censées fonctionner. Jetez un œil aux CommonJs Promesses propositions et Promises/A + spécification pour cela.

Il existe deux concepts de base qui peuvent être mis en œuvre chacun en quelques lignes simples:

  • Une promesse est résolue de manière asynchrone avec le résultat. L'ajout de rappels est une action transparente - indépendamment du fait que la promesse soit déjà résolue ou non, ils seront appelés avec le résultat une fois qu'il sera disponible.

    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;
    }
    
  • Les promesses ont une méthode then qui permet de les enchaîner. Je prends un rappel et renvoie une nouvelle promesse qui sera résolue avec le résultat de ce rappel après avoir été invoqué avec le résultat de la première promesse. Si le rappel renvoie une promesse, il sera assimilé au lieu d'être imbriqué.

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

D'autres concepts consisteraient à conserver un état d'erreur distinct (avec un rappel supplémentaire pour lui) et à intercepter les exceptions dans les gestionnaires, ou à garantir l'asynchronité pour les rappels. Une fois que vous les avez ajoutés, vous disposez d'une implémentation Promise entièrement fonctionnelle.

Voici la chose d'erreur écrite. C'est malheureusement assez répétitif; vous pouvez faire mieux en utilisant des fermetures supplémentaires, mais cela devient vraiment très difficile à comprendre.

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;
};
4
Bergi

Vous voudrez peut-être consulter le article de blog sur Adehun.

Adehun est une implémentation extrêmement légère (environ 166 LOC) et très utile pour apprendre à implémenter la spécification Promise/A +.

Clause de non-responsabilité : J'ai écrit le billet de blog mais le billet de blog explique tout sur Adehun.

La fonction Transition - Gatekeeper for State Transition

Fonction Gatekeeper; garantit que les transitions d'état se produisent lorsque toutes les conditions requises sont remplies.

Si les conditions sont remplies, cette fonction met à jour l'état et la valeur de la promesse. Il déclenche ensuite la fonction de traitement pour un traitement ultérieur.

La fonction de processus exécute la bonne action en fonction de la transition (par exemple, en attente de réalisation) et est expliquée plus loin.

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

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

La fonction Then

La fonction then accepte deux arguments facultatifs (gestionnaires onFulfill et onReject) et doit renvoyer une nouvelle promesse. Deux exigences majeures:

  1. La promesse de base (celle sur laquelle est alors appelée) doit créer une nouvelle promesse en utilisant les gestionnaires passés dans; la base stocke également une référence interne à cette promesse créée afin qu'elle puisse être invoquée une fois la promesse de base remplie/rejetée.

  2. Si la promesse de base est réglée (c'est-à-dire remplie ou rejetée), le gestionnaire approprié doit être appelé immédiatement. Adehun.js gère ce scénario en appelant process dans la fonction then.

''

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;
}`

La fonction Process - Transitions de traitement

Ceci est appelé après les transitions d'état ou lorsque la fonction then est invoquée. Il doit donc vérifier les promesses en attente, car il pourrait avoir été appelé à partir de la fonction then.

Le processus exécute la procédure de résolution de promesse sur toutes les promesses stockées en interne (c'est-à-dire celles qui ont été attachées à la promesse de base via la fonction then) et applique les exigences Promise/A + suivantes:

  1. Appel des gestionnaires de manière asynchrone à l'aide de l'aide de Utils.runAsync (un wrapper fin autour de setTimeout (setImmediate fonctionnera également)).

  2. Création de gestionnaires de secours pour les gestionnaires onSuccess et onReject s'ils sont manquants.

  3. Sélection de la fonction de gestionnaire correcte en fonction de l'état promis, par ex. remplies ou rejetées.

  4. Application du gestionnaire à la valeur de la promesse de base. La valeur de cette opération est transmise à la fonction Résoudre pour terminer le cycle de traitement de la promesse.

  5. Si une erreur se produit, la promesse jointe est immédiatement rejetée.

    function process () {var that = this ,omplFallBack = function (value) {return value; }, rejetteFallBack = fonction (raison) {raison du lancer; };

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

    }

La fonction Résoudre - Résoudre les promesses

C'est probablement la partie la plus importante de la mise en œuvre de la promesse, car elle gère la résolution de la promesse. Il accepte deux paramètres - la promesse et sa valeur de résolution.

Bien qu'il existe de nombreuses vérifications pour diverses valeurs de résolution possibles; les scénarios de résolution intéressants sont deux - ceux impliquant une promesse transmise et un élément exploitable (un objet ayant alors une valeur).

  1. Passer une valeur Promise

Si la valeur de résolution est une autre promesse, alors la promesse doit adopter l'état de cette valeur de résolution. Étant donné que cette valeur de résolution peut être en attente ou réglée, la façon la plus simple de le faire consiste à attacher un nouveau gestionnaire à la valeur de résolution et à y gérer la promesse d'origine. Chaque fois qu'il se règle, la promesse d'origine sera résolue ou rejetée.

  1. Passer une valeur exploitable

Le problème ici est que la fonction then de la valeur thenable ne doit être invoquée qu'une seule fois (une bonne utilisation pour le wrapper once de la programmation fonctionnelle). De même, si la récupération de la fonction then lève une exception, la promesse doit être rejetée immédiatement.

Comme précédemment, la fonction then est invoquée avec des fonctions qui résolvent ou rejettent finalement la promesse, mais la différence ici est l'indicateur appelé qui est défini lors du premier appel et les appels suivants ne sont pas des opérations.

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

Le constructeur de la promesse

Et c'est celui qui rassemble le tout. Les fonctions d'accomplissement et de rejet sont des sucres syntaxiques qui passent des fonctions sans opération pour résoudre et rejeter.

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

J'espère que cela a permis de mieux comprendre le fonctionnement des promesses.

1