web-dev-qa-db-fra.com

Comment accéder aux résultats des promesses précédentes dans une chaîne .then ()?

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?
    });
}
590
Bergi

Harmonie ECMAScript

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é

ECMAScript 8

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
}

ECMAScript 6

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.

ECMAScript 5

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 ).

217
Bergi

Briser la chaîne

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) { … });
348
Bergi

Inspection synchrone

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();
    });
}
97
Esailija

Fermetures de nidification

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
}
51
Bergi

Passage explicite

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.

49
Bergi

Etat contextuel modifiable

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:

  • l'état Mutable est laid , et les variables globales sont mauvaises .
  • Ce modèle ne fonctionne pas au-delà des limites des fonctions, la modularisation des fonctions est plus difficile car leurs déclarations ne doivent pas quitter la portée partagée.
  • La portée des variables n'empêche pas d'y accéder avant leur initialisation. Cela est particulièrement probable pour les constructions de promesses complexes (boucles, ramifications, excursions) où des conditions de concurrence pourraient se présenter. Passer explicitement l’état, un conception déclarative qui encourage, promet d’encourager un style de codage plus propre qui puisse empêcher cela.
  • Il faut choisir correctement la portée de ces variables partagées. Il doit être local à la fonction exécutée pour éviter les conditions de concurrence entre plusieurs invocations parallèles, comme ce serait le cas si, par exemple, l'état était stocké sur une instance.

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

Une version moins dure de "l'état contextuel modifiable"

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);
    });
}
  • Les variables globales sont mauvaises, cette solution utilise donc une variable de portée locale qui ne cause aucun dommage. Il est uniquement accessible dans la fonction.
  • L'état mutable est moche, mais cela ne mute pas de manière moche. L'état déplaisant laide fait traditionnellement référence à la modification de l'état des arguments de fonction ou des variables globales, mais cette approche modifie simplement l'état d'une variable de portée locale qui existe dans le seul but d'agréger les résultats de promesse ... une variable qui mourra d'une simple mort une fois la promesse résolue.
  • Les promesses intermédiaires ne sont pas empêchées d'accéder à l'état de l'objet de résultats, mais cela n'introduit pas de scénario effrayant dans lequel l'une des promesses de la chaîne va devenir malveillante et saboter vos résultats. La responsabilité de définir les valeurs à chaque étape de la promesse est limitée à cette fonction et le résultat global sera soit correct, soit incorrect. Ce ne sera pas un bug qui apparaîtra des années plus tard dans la production (à moins que vous ne le souhaitiez. !)
  • Cela n'introduit pas de scénario de condition de concurrence qui résulterait d'un appel parallèle, car une nouvelle instance de la variable de résultats est créée pour chaque appel de la fonction getExample.
11
Jay

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!

7
Antoine

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

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é!

5
Antoine

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

Mise à jour: exemple de travail ajouté

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>
2
amaksr

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

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:

http://bluebirdjs.com/docs/api/promise.bind.html

1
alphakevin
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

1
Minh Giang

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

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.)

0
David Spector