J'ai restructuré mon code pour promesses , et construit une longue et longue chaîne de promesses plate , composée de plusieurs .then()
rappels. En fin de compte, je veux renvoyer une valeur composite et je dois accéder à plusieurs résultats de promesse intermédiaires . Cependant, les valeurs de résolution du milieu de la séquence ne sont pas comprises dans le dernier rappel, comment puis-je y accéder?
function getExample() {
return promiseA(…).then(function(resultA) {
// Some processing
return promiseB(…);
}).then(function(resultB) {
// More processing
return // How do I gain access to resultA here?
});
}
Bien sûr, ce problème a également été reconnu par les concepteurs de langage. Ils ont fait beaucoup de travail et le proposition de fonctions asynchrones l'a finalement intégré
Vous n'avez plus besoin d'une seule invocation ou fonction de rappel then
, car dans une fonction asynchrone (qui renvoie une promesse lors de l'appel), vous pouvez simplement attendre que les promesses soient résolues directement. Il comporte également des structures de contrôle arbitraires telles que des conditions, des boucles et des clauses try-catch-catch, mais pour des raisons de commodité, nous n'en avons pas besoin ici:
async function getExample() {
var resultA = await promiseA(…);
// some processing
var resultB = await promiseB(…);
// more processing
return // something using both resultA and resultB
}
En attendant ES8, nous utilisions déjà un type de syntaxe très similaire. ES6 est venu avec fonctions génératrices , ce qui permet de diviser l'exécution en morceaux à des mots-clés yield
placés de façon arbitraire. Ces tranches peuvent être exécutées les unes après les autres, de manière indépendante, voire asynchrone - et c'est exactement ce que nous faisons lorsque nous voulons attendre une résolution de promesse avant d'exécuter l'étape suivante.
Il existe des bibliothèques dédiées (comme co ou task.js ), mais de nombreuses bibliothèques de promesses ont des fonctions d'assistance ( Q , Bluebird , when ,…) cela fait cette exécution asynchrone étape par étape pour vous lorsque vous leur donnez une fonction de générateur qui donne des promesses.
var getExample = Promise.coroutine(function* () {
// ^^^^^^^^^^^^^^^^^ Bluebird syntax
var resultA = yield promiseA(…);
// some processing
var resultB = yield promiseB(…);
// more processing
return // something using both resultA and resultB
});
Cela fonctionnait dans Node.js depuis la version 4.0, également quelques navigateurs (ou leurs éditions dev) supportaient relativement tôt la syntaxe de générateur.
Cependant, si vous voulez/avez besoin d'une compatibilité ascendante, vous ne pouvez pas utiliser ceux sans transpiler. Les outils générés, ainsi que les fonctions asynchrones, sont pris en charge par les outils actuels. Voir, par exemple, la documentation de Babel sur générateurs et fonctions asynchrones .
Et puis, il y a aussi beaucoup d'autres langages de compilation vers JS qui sont dédiés à faciliter la programmation asynchrone. Ils utilisent généralement une syntaxe similaire à await
, (par exemple Iced CoffeeScript ), mais il en existe aussi qui comportent une notation do
de type Haskell (par exemple LatteJs , monadique , PureScript ou LispyScript ).
Lorsque vous devez accéder aux valeurs intermédiaires de votre chaîne, vous devez diviser votre chaîne en plusieurs parties dont vous avez besoin. Au lieu d'attacher un rappel et d'essayer d'une manière ou d'une autre d'utiliser ses paramètres à plusieurs reprises, attachez plusieurs rappels à la même promesse - partout où vous avez besoin de la valeur du résultat. N'oubliez pas qu'un la promesse représente simplement une valeur future) (= mandataire ! En plus de dériver une promesse de l’autre dans une chaîne linéaire, utilisez les combinateurs de promesse qui vous sont fournis par votre bibliothèque pour générer la valeur du résultat.
Cela se traduira par un flux de contrôle très simple, une composition claire des fonctionnalités et donc une modularisation aisée.
function getExample() {
var a = promiseA(…);
var b = a.then(function(resultA) {
// some processing
return promiseB(…);
});
return Promise.all([a, b]).then(function([resultA, resultB]) {
// more processing
return // something using both resultA and resultB
});
}
Au lieu du paramètre destructuration dans le rappel après Promise.all
qui n'est devenu disponible qu'avec ES6, dans ES5, l'appel then
serait remplacé par une méthode d'assistance astucieuse fournie par de nombreuses bibliothèques de promesses ( Q , Bluebird , quand ,…): .spread(function(resultA, resultB) { …
.
Bluebird propose également une combinaison join
_ fonction pour remplacer cette Promise.all
+ spread
combinaison avec une construction plus simple (et plus efficace):
…
return Promise.join(a, b, function(resultA, resultB) { … });
Affectation de valeurs promises aux variables nécessaires ultérieurement, puis obtention de leur valeur via une inspection synchrone. L'exemple utilise la méthode .value()
de bluebird, mais de nombreuses bibliothèques fournissent une méthode similaire.
function getExample() {
var a = promiseA(…);
return a.then(function() {
// some processing
return promiseB(…);
}).then(function(resultB) {
// a is guaranteed to be fulfilled here so we can just retrieve its
// value synchronously
var aValue = a.value();
});
}
Ceci peut être utilisé pour autant de valeurs que vous le souhaitez:
function getExample() {
var a = promiseA(…);
var b = a.then(function() {
return promiseB(…)
});
var c = b.then(function() {
return promiseC(…);
});
var d = c.then(function() {
return promiseD(…);
});
return d.then(function() {
return a.value() + b.value() + c.value() + d.value();
});
}
L'utilisation de fermetures pour conserver l'étendue des variables (dans notre cas, les paramètres de la fonction de rappel de réussite) constitue la solution JavaScript naturelle. Avec des promesses, nous pouvons arbitrairement imbriquer et aplatir.then()
callbacks - ils sont sémantiquement équivalents, à l'exception de la portée de celle interne.
function getExample() {
return promiseA(…).then(function(resultA) {
// some processing
return promiseB(…).then(function(resultB) {
// more processing
return // something using both resultA and resultB;
});
});
}
Bien sûr, il s'agit de construire une pyramide d'indentation. Si l'indentation devient trop grande, vous pouvez toujours utiliser les anciens outils pour contrer la pyramide de Doom : modulariser, utiliser des fonctions nommées supplémentaires et aplatir la chaîne de promesses dès que vous n'avez pas besoin de variable pas plus.
En théorie, vous pouvez toujours éviter plus de deux niveaux d’imbrication (en rendant toutes les fermetures explicites), dans la pratique, utilisez-en autant qu’ils sont raisonnables.
function getExample() {
// preprocessing
return promiseA(…).then(makeAhandler(…));
}
function makeAhandler(…)
return function(resultA) {
// some processing
return promiseB(…).then(makeBhandler(resultA, …));
};
}
function makeBhandler(resultA, …) {
return function(resultB) {
// more processing
return // anything that uses the variables in scope
};
}
Vous pouvez également utiliser des fonctions auxiliaires pour ce type de application partielle , comme _.partial
de nderscore / lodash ou le méthode native .bind()
, pour diminuer davantage l'indentation:
function getExample() {
// preprocessing
return promiseA(…).then(handlerA);
}
function handlerA(resultA) {
// some processing
return promiseB(…).then(handlerB.bind(null, resultA));
}
function handlerB(resultA, resultB) {
// more processing
return // anything that uses resultA and resultB
}
Semblable à l'imbrication des rappels, cette technique repose sur les fermetures. Cependant, la chaîne reste plate: au lieu de ne transmettre que le dernier résultat, un objet d'état est transmis pour chaque étape. Ces objets d'état accumulent les résultats des actions précédentes et transmettent toutes les valeurs nécessaires ultérieurement, ainsi que le résultat de la tâche en cours.
_function getExample() {
return promiseA(…).then(function(resultA) {
// some processing
return promiseB(…).then(b => [resultA, b]); // function(b) { return [resultA, b] }
}).then(function([resultA, resultB]) {
// more processing
return // something using both resultA and resultB
});
}
_
Ici, cette petite flèche _b => [resultA, b]
_ est la fonction qui se ferme sur resultA
et qui passe un tableau des deux résultats à l’étape suivante. Qui utilise la syntaxe de déstructuration des paramètres pour le décomposer à nouveau en variables uniques.
Avant que la déstructuration devienne disponible avec ES6, une méthode d'assistance astucieuse appelée .spread()
était fournie par de nombreuses bibliothèques de promesses ( Q , Bluebird , when ,…). Il faut une fonction avec plusieurs paramètres - un pour chaque élément du tableau - à utiliser comme .spread(function(resultA, resultB) { …
.
Bien sûr, cette fermeture nécessaire ici peut être davantage simplifiée par certaines fonctions d'assistance, par exemple.
_function addTo(x) {
// imagine complex `arguments` fiddling or anything that helps usability
// but you get the idea with this simple one:
return res => [x, res];
}
…
return promiseB(…).then(addTo(resultA));
_
Sinon, vous pouvez utiliser _Promise.all
_ pour produire la promesse du tableau:
_function getExample() {
return promiseA(…).then(function(resultA) {
// some processing
return Promise.all([resultA, promiseB(…)]); // resultA will implicitly be wrapped
// as if passed to Promise.resolve()
}).then(function([resultA, resultB]) {
// more processing
return // something using both resultA and resultB
});
}
_
Et vous pourriez non seulement utiliser des tableaux, mais des objets arbitrairement complexes. Par exemple, avec _.extend
ou Object.assign
dans une fonction d'assistance différente:
_function augment(obj, name) {
return function (res) { var r = Object.assign({}, obj); r[name] = res; return r; };
}
function getExample() {
return promiseA(…).then(function(resultA) {
// some processing
return promiseB(…).then(augment({resultA}, "resultB"));
}).then(function(obj) {
// more processing
return // something using both obj.resultA and obj.resultB
});
}
_
Bien que ce modèle garantisse une chaîne plate et que les objets d'état explicites puissent améliorer la clarté, il deviendra fastidieux pour une longue chaîne. Surtout lorsque vous avez besoin de l'état de manière sporadique, vous devez toujours le traverser à chaque étape. Avec cette interface fixe, les rappels uniques dans la chaîne sont plutôt étroitement liés et inflexibles au changement. Cela rend plus difficile la factorisation des étapes individuelles et les rappels ne peuvent pas être fournis directement à partir d'autres modules - ils doivent toujours être enveloppés dans du code standard qui tient compte de l'état. Les fonctions d'assistance abstraites comme ci-dessus peuvent soulager un peu la douleur, mais elles seront toujours présentes.
La solution triviale (mais peu élégante et plutôt sujette aux erreurs) consiste simplement à utiliser des variables de plus haute portée (auxquelles tous les callbacks ont accès) et à leur écrire des valeurs de résultat lorsque vous les recevez:
function getExample() {
var resultA;
return promiseA(…).then(function(_resultA) {
resultA = _resultA;
// some processing
return promiseB(…);
}).then(function(resultB) {
// more processing
return // something using both resultA and resultB
});
}
Au lieu de nombreuses variables, vous pouvez également utiliser un objet (initialement vide) sur lequel les résultats sont stockés sous forme de propriétés créées dynamiquement.
Cette solution présente plusieurs inconvénients:
La bibliothèque Bluebird encourage l'utilisation d'un objet qui est transmis en utilisant leur méthode bind()
pour affecter un objet de contexte à une chaîne de promesse. Il sera accessible à partir de chaque fonction de rappel via le paramètre autrement inutilisable mot-clé this
. Bien que les propriétés des objets soient plus sujettes aux fautes de frappe non détectées qu'aux variables, le modèle est assez astucieux:
function getExample() {
return promiseA(…)
.bind({}) // Bluebird only!
.then(function(resultA) {
this.resultA = resultA;
// some processing
return promiseB(…);
}).then(function(resultB) {
// more processing
return // something using both this.resultA and resultB
}).bind(); // don't forget to unbind the object if you don't want the
// caller to access it
}
Cette approche peut être facilement simulée dans des bibliothèques de promesses qui ne prennent pas en charge .bind (bien que d'une manière un peu plus détaillée et ne pouvant pas être utilisé dans une expression):
function getExample() {
var ctx = {};
return promiseA(…)
.then(function(resultA) {
this.resultA = resultA;
// some processing
return promiseB(…);
}.bind(ctx)).then(function(resultB) {
// more processing
return // something using both this.resultA and resultB
}.bind(ctx));
}
Utiliser un objet de portée locale pour collecter les résultats intermédiaires dans une chaîne de promesses constitue une approche raisonnable de la question que vous avez posée. Considérez l'extrait suivant:
function getExample(){
//locally scoped
const results = {};
return promiseA(paramsA).then(function(resultA){
results.a = resultA;
return promiseB(paramsB);
}).then(function(resultB){
results.b = resultB;
return promiseC(paramsC);
}).then(function(resultC){
//Resolve with composite of all promises
return Promise.resolve(results.a + results.b + resultC);
}).catch(function(error){
return Promise.reject(error);
});
}
Le nœud 7.4 prend désormais en charge les appels asynchrones/en attente avec l'indicateur d'harmonie.
Essaye ça:
async function getExample(){
let response = await returnPromise();
let response2 = await returnPromise2();
console.log(response, response2)
}
getExample()
et lancez le fichier avec:
node --harmony-async-await getExample.js
Aussi simple que peut être!
Ces jours-ci, je dois aussi répondre à quelques questions comme vous. Enfin, je trouve une bonne solution avec la question, c'est simple et bon à lire. J'espère que ceci peut vous aider.
Selon comment-chaîne-promesses javascript
ok, regardons le code:
const firstPromise = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('first promise is completed');
resolve({data: '123'});
}, 2000);
});
};
const secondPromise = (someStuff) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('second promise is completed');
resolve({newData: `${someStuff.data} some more data`});
}, 2000);
});
};
const thirdPromise = (someStuff) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('third promise is completed');
resolve({result: someStuff});
}, 2000);
});
};
firstPromise()
.then(secondPromise)
.then(thirdPromise)
.then(data => {
console.log(data);
});
Autre réponse, en utilisant babel-node
version <6
Utiliser async - await
npm install -g [email protected]
example.js:
async function getExample(){
let response = await returnPromise();
let response2 = await returnPromise2();
console.log(response, response2)
}
getExample()
Ensuite, lancez babel-node example.js
et le tour est joué!
Une autre réponse, en utilisant séquenceur nsynjs :
function getExample(){
var response1 = returnPromise1().data;
// promise1 is resolved at this point, '.data' has the result from resolve(result)
var response2 = returnPromise2().data;
// promise2 is resolved at this point, '.data' has the result from resolve(result)
console.log(response, response2);
}
nynjs.run(getExample,{},function(){
console.log('all done');
})
function synchronousCode() {
var urls=[
"https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js",
"https://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js",
"https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"
];
for(var i=0; i<urls.length; i++) {
var len=window.fetch(urls[i]).data.text().data.length;
// ^ ^
// | +- 2-nd promise result
// | assigned to 'data'
// |
// +-- 1-st promise result assigned to 'data'
//
console.log('URL #'+i+' : '+urls[i]+", length: "+len);
}
}
nsynjs.run(synchronousCode,{},function(){
console.log('all done');
})
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>
Je ne vais pas utiliser ce modèle dans mon propre code car je ne suis pas un grand fan des variables globales. Cependant, à la rigueur, cela fonctionnera.
L'utilisateur est un modèle de Mongoose promis.
var globalVar = '';
User.findAsync({}).then(function(users){
globalVar = users;
}).then(function(){
console.log(globalVar);
});
Lorsque vous utilisez bluebird, vous pouvez utiliser la méthode .bind
pour partager des variables dans la chaîne de promesses:
somethingAsync().bind({})
.spread(function (aValue, bValue) {
this.aValue = aValue;
this.bValue = bValue;
return somethingElseAsync(aValue, bValue);
})
.then(function (cValue) {
return this.aValue + this.bValue + cValue;
});
s'il vous plaît vérifier ce lien pour plus d'informations:
function getExample() {
var retA, retB;
return promiseA(…).then(function(resultA) {
retA = resultA;
// Some processing
return promiseB(…);
}).then(function(resultB) {
// More processing
//retA is value of promiseA
return // How do I gain access to resultA here?
});
}
moyen facile: D
Je pense que vous pouvez utiliser le hash de RSVP.
Quelque chose comme comme ci-dessous:
const mainPromise = () => {
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('first promise is completed');
resolve({data: '123'});
}, 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('second promise is completed');
resolve({data: '456'});
}, 2000);
});
return new RSVP.hash({
prom1: promise1,
prom2: promise2
});
};
mainPromise()
.then(data => {
console.log(data.prom1);
console.log(data.prom2);
});
Solution:
Vous pouvez mettre explicitement les valeurs intermédiaires dans la portée de toute fonction "then" ultérieure, en utilisant "bind". C'est une solution intéressante qui ne nécessite pas de modifier le fonctionnement de Promises et qui ne nécessite qu'une ligne ou deux de code pour propager les valeurs, tout comme les erreurs sont déjà propagées.
Voici un exemple complet:
// Get info asynchronously from a server
function pGetServerInfo()
{
// then value: "server info"
} // pGetServerInfo
// Write into a file asynchronously
function pWriteFile(path,string)
{
// no then value
} // pWriteFile
// The heart of the solution: Write formatted info into a log file asynchronously,
// using the pGetServerInfo and pWriteFile operations
function pLogInfo(localInfo)
{
var scope={localInfo:localInfo}; // Create an explicit scope object
var thenFunc=p2.bind(scope); // Create a temporary function with this scope
return (pGetServerInfo().then(thenFunc)); // Do the next 'then' in the chain
} // pLogInfo
// Scope of this 'then' function is {localInfo:localInfo}
function p2(serverInfo)
{
// Do the final 'then' in the chain: Writes "local info, server info"
return pWriteFile('log',this.localInfo+','+serverInfo);
} // p2
Cette solution peut être invoquée comme suit:
pLogInfo("local info").then().catch(err);
(Remarque: une version plus complexe et complète de cette solution a été testée, mais pas cette version d'exemple, elle pourrait donc avoir un bogue.)