Comment une bibliothèque de type promesse / différer comme q est-elle implémentée? J'essayais de lire le code source, mais je trouvais cela difficile à comprendre. Je me suis donc dit que ce serait bien que quelqu'un puisse m'expliquer, de haut niveau, quelles sont les techniques utilisées pour mettre en œuvre les promesses dans des environnements JS à un seul thread. comme Node et les navigateurs.
Réponses
Trop de publicités?Je le trouve plus difficile à expliquer que pour montrer un exemple, voici donc une très simple de mise en œuvre de ce qu'un reporter/promesse pourrait être.
Avertissement: Ce n'est pas une fonctionnelle de la mise en œuvre et de certaines parties de la Promesse/Un cahier des charges sont manquants, C'est juste pour expliquer la base de promesses.
tl;dr: Aller à la Créer des classes et de l'exemple de la section pour voir la pleine mise en œuvre.
La promesse:
Nous devons d'abord créer un objet promise à un tableau de rappels. Je vais commencer à travailler avec des objets parce que c'est plus clair:
var promise = {
callbacks: []
}
maintenant, ajoutez des rappels avec la méthode alors:
var promise = {
callbacks: [],
then: function (callback) {
callbacks.push(callback);
}
}
Et nous avons besoin de l'erreur rappels:
var promise = {
okCallbacks: [],
koCallbacks: [],
then: function (okCallback, koCallback) {
okCallbacks.push(okCallback);
if (koCallback) {
koCallbacks.push(koCallback);
}
}
}
Reporter:
Maintenant, créez le reporter de l'objet qui aura une promesse:
var defer = {
promise: promise
};
Le reporter doit être résolu:
var defer = {
promise: promise,
resolve: function (data) {
this.promise.okCallbacks.forEach(function(callback) {
window.setTimeout(function () {
callback(data)
}, 0);
});
},
};
Et les besoins de 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 timout pour permettre que le code sera toujours asynchromous.
Et qu'est ce qu'une base de reporter/promesse de la mise en œuvre des besoins.
Créer des classes et de l'exemple:
Maintenant permet de convertir à la fois des objets de 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 reporter:
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) {
deferred.resolve(request.responseText);
} else {
deferred.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 grandira quand vous ajoutez d'autres options, par exemple les multiples promesses de résolution:
Defer.all(promiseA, promiseB, promiseC).then()
ou la promesse de chaîne:
getUserById(id).then(getFilesByUser).then(deleteFile).then(promptResult);
Pour en savoir plus sur le cahier des charges: CommonJS Promesse de Spécification. Notez que les principales bibliothèques (Q, when.js, rsvp.js, nœud-promesse, ...), suivre les Promesses/Un cahier des charges.
Espère que j'ai été clair enaugh.
Edit:
Comme demandé dans les commentaires, j'ai ajouté deux choses dans cette version:
- La possibilité d'appel, puis d'une promesse, quel que soit le statut.
- La capacité de la chaîne de promesses.
Pour être en mesure d'appeler la promesse lorsque résolu, vous devez ajouter le statut de la promesse, et lorsque le puis est appelé à vérifier le statut. Si le statut est résolu ou rejeté exécutez simplement le rappel de ses données ou erreur.
Pour être en mesure à la chaîne des promesses, vous devez générer un nouveau différer pour chaque appel à l' then
et, lorsque la promesse est résolu/a rejeté, de résoudre ou de rejeter la nouvelle promesse avec le résultat de la fonction de rappel. Ainsi, lorsque la promesse est faite, si le callback retourne une nouvelle promesse, il est lié à la promesse retourné avec l' then()
. Si non, la promesse est résolu avec le résultat de la fonction de rappel.
Ici, c'est 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.resolveCallback({
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 reporter:
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 resoved/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 grandi un peu.
Q est très complexe de la promesse de la bibliothèque en termes de mise en œuvre, car elle vise à soutenir le pipelining et RPC type de scénarios. J'ai mon propre os à nu de la mise en œuvre des Promesses/A+ spécification ici.
En principe c'est assez simple. Avant que la promesse est réglé/résolu, vous gardez une trace de tout rappel ou errbacks en les poussant dans un tableau. Lorsque la promesse est réglée, vous appeler les rappels ou errbacks et d'enregistrer ce résultat que la promesse a été réglé avec (et si elle a été accomplie ou rejeté). Après c'est réglé, il vous suffit d'appeler le callback ou errbacks avec les résultats enregistrés.
Qui vous donne environ la sémantique de l' done
. Pour construire then
vous suffit de le retourner une nouvelle promesse qui est résolu avec le résultat de l'appel de l'rappels/errbacks.
Si vous êtes intéressé par un plein explenation de la reasonning derrière le développement d'un plein sur la promesse de la mise en œuvre avec le soutien pour le RPC et le pipelining comme Q, vous pouvez lire kriskowal de reasonning ici. C'est vraiment une très belle approche graduée que je ne peux pas le recommander assez fortement, si vous songez à la mise en œuvre de promesses. Il est probablement plus intéressant à lire même si vous êtes simplement à l'aide d'une promesse de la bibliothèque.
Que Forbes mentionne dans sa réponse, j'ai fait la chronique de nombreuses décisions de conception impliqués dans la réalisation d'une bibliothèque comme Q, ici https://github.com/kriskowal/q/blob/master/design/README.js. Qu'il suffise de dire, il y a des niveaux d'une promesse de la bibliothèque, et beaucoup de bibliothèques qui s'arrêtent à différents niveaux.
Au premier niveau, capturé par les Promesses/A+ spécification, une promesse est un proxy pour un résultat final et est adapté pour la gestion locale"asynchronie". C'est, il convient de s'assurer que le travail se produit dans le bon ordre, et de s'assurer qu'il est simple et directe pour écouter le résultat d'une opération, indépendamment de savoir si elle est déjà réglée, ou va se produire dans l'avenir. Il rend également qu'il est tout aussi simple pour une ou plusieurs parties à s'abonner à un résultat final.
Q, comme je l'ai mis en œuvre, fournit des promesses que les procurations éventuelles, à distance, ou de l'éventuelle+à distance des résultats. À cette fin, il est de conception est inversé, avec différentes implémentations pour des promesses différée, des promesses, des promesses tenues, rejeté des promesses et des promesses pour les objets distants (le dernier étant mis en œuvre dans Q-Connexion). Ils partagent tous la même interface et le travail en envoyant et en recevant des messages comme "alors" (ce qui est suffisant pour des Promesses/A+), mais aussi "get" et "invoquer". Ainsi, Q est sur "distribué asynchronie", et existe sur un autre calque.
Toutefois, Q a été prise vers le bas à partir d'une couche supérieure, où les promesses sont utilisés pour la gestion distribuée de l'asynchronie entre mutuellement suspect parties comme vous, un commerçant, une banque, Facebook, le gouvernement, pas des ennemis, peut-être même des amis, mais parfois avec des conflits d'intérêts. Le Q que j'ai mis en place est conçu pour être API compatible avec une sécurité renforcée des promesses (qui est la raison de la séparation d' promise
et resolve
), avec l'espoir qu'il serait de présenter les gens les promesses, les former à l'utilisation de cette API, et de leur permettre de prendre leur code avec eux s'ils ont besoin d'utiliser des promesses dans la sécurité des applications web hybrides dans l'avenir.
Bien sûr, il existe des échanges que vous vous déplacez les couches, généralement en vitesse. Donc, les promesses implémentations peuvent également être conçu de manière à co-exister. C'est là que le concept de "thenable" entre. Promesse de bibliothèques au niveau de chaque couche peut être conçu pour consommer promet de n'importe quelle autre couche, de sorte que plusieurs implémentations peuvent coexister, et les utilisateurs peuvent acheter uniquement ce dont ils ont besoin.
Tout cela est dit, il n'y a aucune excuse pour être difficile à lire. Dominique et moi-même travaillons sur une version de Q, qui sera plus modulaire et accessible, avec certains de ses distraire des dépendances et des solutions de rechange déplacés dans d'autres modules et packages. Heureusement que des gens comme Forbes, Crockford, et d'autres ont comblé la lacune de l'éducation en rendant plus simple de bibliothèques.
D'abord assurez-vous de comprendre comment les Promesses sont censés travailler. Jetez un oeil à la CommonJs Promesses propositions et les Promesses/A+ spécifications pour.
Il y a deux concepts de base qui peuvent être mis en œuvre chaque en quelques lignes:
-
Une Promesse fait de manière asynchrone résolu avec le résultat. L'ajout de rappels est transparente l'action indépendante de celle de savoir si la promesse est déjà résolu ou pas, ils seront appelés avec le résultat une fois qu'il est 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; }
-
Des promesses ont un
then
méthode qui permet le chaînage. Je prend un callback et retourne une nouvelle Promesse qui sera résolu avec la suite de ce rappel après qu'il a été appelé avec la première promesse de résultat. Si la fonction de rappel renvoie une Promesse, il sera assimilé au lieu de se imbriqués.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; };
Autres notions seront entretien d'un erreur de l'état (avec un supplément de rappel pour elle) et d'intercepter les exceptions aux gestionnaires, ou la garantie asynchronity pour les rappels. Une fois que vous ajoutez ceux-ci, vous avez entièrement fonctionnelle d'une Promesse de la mise en œuvre.
Ici, l'erreur est chose écrite. Malheureusement il est assez répétitif; vous pouvez faire mieux en utilisant les fermetures, mais alors il est vraiment vraiment dur à 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;
};
- Je me qualifier comme une promesse haineux,sans doute, mais je suis l'auteur d'une promesse module équivalent appelées des récoltes qui a quelques centaines de lignes de code et rien de complexités trouvé dans la promesse des bibliothèques: https://github.com/salboaie/harvests
L'idée dans les récoltes est assez simple, pour chaque 'attendre' la variable gardez une liste avec les appels qui doivent être apportées. Pour chaque appel doit être fait, de garder la liste de problèmes non résolus variables. Lorsqu'une variable suis résolu, coup d'oeil sur les appels et faire des appels pour qui toutes les dépendances sont résolues.
Assez simple et il n'y pas de wrapper de créer des objets comme avec des promesses. Certaines personnes se coincer avec des promesses idée car il a l'air sur la surface que vous pouvez avoir un objet à partir de l'avenir, mais c'est le genre de mal. Vous ne pouvez pas vraiment faire quelque chose avec les objets de l'avenir, ils n'ont pas une structure,etc. Une promesse essaie d'être un proxy, mais n'est pas un vrai proxy car ne pouvez pas deviner ce que l'objet réel ressemble.
Très rapide, simple, pas de complexité ajoutée à votre code, juste parce que vous pouvez faire semblant d'avoir des objets à partir d'avenir...