Question Comment accéder aux résultats de la promesse précédente dans une chaîne .then ()?


J'ai restructuré mon code pour promesses, et construit un merveilleux long chaîne de promesse plate, composé de plusieurs .then() rappels. À la fin, je veux retourner une valeur composite, et besoin d'accéder à plusieurs résultats prometteurs intermédiaires. Cependant, les valeurs de résolution du milieu de la séquence ne sont pas prises en compte 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?
    });
}

509
2018-01-31 10:41


origine


Réponses:


Briser la chaîne

Lorsque vous avez besoin d'accéder aux valeurs intermédiaires de votre chaîne, vous devez séparer votre chaîne en pièces uniques dont vous avez besoin. Au lieu d'attacher un rappel et d'essayer d'utiliser son paramètre plusieurs fois, attachez plusieurs rappels à la même promesse - partout où vous avez besoin de la valeur du résultat. Ne pas oublier, un promesse représente juste (procurations) une valeur future! Suivant la dérivation d'une promesse de l'autre dans une chaîne linéaire, utilisez les combinateurs de promesses qui vous sont donnés 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 déstructuration dans le rappel après Promise.all qui est seulement disponible avec ES6, dans ES5 le then appel serait remplacé par une méthode d'assistance astucieuse qui a été fournie par de nombreuses bibliothèques de promesses (Q, Oiseau bleu, quand, ...): .spread(function(resultA, resultB) { ….

Bluebird dispose également d'un dédié join fonction pour remplacer cela Promise.all+spread combinaison avec une construction plus simple (et plus efficace):

…
return Promise.join(a, b, function(resultA, resultB) { … });

306
2018-01-31 10:44



ECMAScript Harmony

Bien sûr, ce problème a également été reconnu par les concepteurs de langage. Ils ont fait beaucoup de travail et proposition de fonctions asynchrones finalement fait en

ECMAScript 8

Vous n'avez pas besoin d'un seul then invocation ou fonction de rappel, comme 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 comme des conditions, des boucles et des clauses try-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

Pendant que nous attendions ES8, nous avons déjà utilisé un type de syntaxe très similaire. ES6 est venu avec fonctions du générateur, qui permettent de casser l'exécution en morceaux arbitrairement placés yield mots clés. Ces tranches peuvent être exécutées les unes après les autres, indépendamment, même de manière asynchrone - et c'est exactement ce que nous faisons lorsque nous voulons attendre une résolution de la promesse avant de passer à l'étape suivante.

Il existe des bibliothèques dédiées (comme co ou task.js), mais aussi de nombreuses bibliothèques de promesses ont des fonctions d'aide (Q, Oiseau bleu, quand, ...) qui font cette exécution étape par étape asynchrone pour vous quand vous leur donnez une fonction génératrice 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 a fonctionné dans Node.js depuis la version 4.0, aussi quelques navigateurs (ou leurs éditions de développement) ont supporté la syntaxe du générateur relativement tôt.

ECMAScript 5

Cependant, si vous voulez / devez être rétrocompatible, vous ne pouvez pas les utiliser sans transpiler. Les fonctions génératrices et asynchrones sont supportées par l'outillage actuel, voir par exemple la documentation de Babel sur générateurs et fonctions asynchrones.

Et puis, il y a aussi beaucoup d'autres compiler vers les langages JS qui sont dédiés à l'accélération de la programmation asynchrone. Ils utilisent généralement une syntaxe similaire à await, (par exemple. CoffeeScript glacé), mais il y en a aussi d'autres qui ressemblent à un Haskell do-notation (par ex. LatteJs, monadique, PureScript ou LispyScript).


190
2018-01-31 10:43



Inspection synchrone

Assigner des valeurs de promesses-pour-plus-tard-nécessaires à des variables, puis obtenir leur valeur via une inspection synchrone. L'exemple utilise les bluebirds .value() méthode 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();
    });
}

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

86
2018-01-31 13:16



Nidification (et) fermetures

L'utilisation de fermetures pour maintenir la portée des variables (dans notre cas, les paramètres de la fonction de rappel de succès) est la solution JavaScript naturelle. Avec des promesses, nous pouvons arbitrairement nicher et aplatir  .then() callbacks - ils sont sémantiquement équivalents, sauf pour la portée de l'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, cela construit une pyramide d'indentation. Si l'indentation devient trop grande, vous pouvez toujours appliquer les anciens outils pour contrer la pyramide de malheur: modulariser, utiliser des fonctions supplémentaires, et aplatir la chaîne de promesses dès que vous n'avez plus besoin d'une variable.
En théorie, vous pouvez toujours éviter plus de deux niveaux d'imbrication (en rendant toutes les fermetures explicites), en pratique, en utiliser autant que possible.

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 d'assistance pour ce type de application partielle, comme _.partial de Souligner/lodash ou la originaire de .bind() méthode, 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
}

50
2018-01-31 10:42



Passage explicite

Similaire à l'imbrication des rappels, cette technique repose sur des fermetures. Cependant, la chaîne reste à plat - au lieu de transmettre uniquement 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, en transmettant toutes les valeurs qui seront nécessaires plus tard encore plus 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 ferme sur resultAet passe un tableau des deux résultats à l'étape suivante. Qui utilise la syntaxe de déstructuration des paramètres pour la décomposer à nouveau en variables uniques.

Avant que la déstructuration soit disponible avec ES6, une méthode d'assistance astucieuse appelée .spread() a été fourni par de nombreuses bibliothèques de promesses (Q, Oiseau bleu, quand, ...). Il prend une fonction avec plusieurs paramètres - un pour chaque élément de tableau - à utiliser comme .spread(function(resultA, resultB) { ….

Bien entendu, cette fermeture nécessaire peut être encore simplifiée par certaines fonctions d'assistance, par ex.

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

Alternativement, vous pouvez employer Promise.all pour produire la promesse pour le 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 pouvez non seulement utiliser des tableaux, mais des objets arbitrairement complexes. Par exemple, avec _.extend ou Object.assign dans une autre fonction d'assistance:

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 quand vous n'avez besoin de l'état que sporadiquement, vous devez toujours passer à travers chaque étape. Avec cette interface fixe, les rappels uniques de la chaîne sont plutôt étroitement liés et rigides au changement. Cela rend l'affacturage plus simple, et les rappels ne peuvent pas être fournis directement à partir d'autres modules - ils doivent toujours être enveloppés dans un code standard qui se soucie de l'état. Les fonctions d'aide abstraites comme ci-dessus peuvent atténuer un peu la douleur, mais elles seront toujours présentes.


42
2018-01-31 10:42



Etat contextuel Mutable

La solution triviale (mais inélégante et plutôt erronée) consiste simplement à utiliser des variables de plus grande portée (auxquelles tous les rappels de la chaîne ont accès) et à leur écrire des valeurs de résultat lorsque vous les obtenez:

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, on peut aussi utiliser un objet (initialement vide), sur lequel les résultats sont stockés en tant que propriétés créées dynamiquement.

Cette solution a plusieurs inconvénients:

  • L'état Mutable est moche, et les variables globales sont le mal.
  • Ce modèle ne fonctionne pas à travers les frontières de 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, branchements, excusions) où les conditions de course peuvent se produire. Passant l'état explicitement, un design déclaratif qui promet d'encourager, force un style de codage plus propre qui peut 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 empêcher les conditions de concurrence entre plusieurs appels 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 transmis, en utilisant leur bind() méthode pour affecter un objet de contexte à une chaîne de promesses. Il sera accessible depuis chaque fonction de rappel via le mode inutilisable this mot-clé. Bien que les propriétés des objets soient plus sujettes aux fautes de frappe non détectées que les variables, le modèle est assez intelligent:

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 les bibliothèques de promesses qui ne supportent pas .bind (bien que d'une manière un peu plus verbeuse et ne peut pas être utilisée 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));
}

28
2018-01-31 10:43



Le nœud 7.4 supporte maintenant les appels async / await avec le drapeau d'harmonie.

Essaye ça:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

et exécutez le fichier avec:

node --harmony-async-await getExample.js

Aussi simple que possible!


5
2018-01-21 22:14



Une autre réponse, en utilisant babel-node version <6

En utilisant async - await 

npm install -g babel@5.6.14

example.js:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

Ensuite, cours babel-node example.js et voilà!


4
2017-11-20 19:59



Un spin moins dur sur "Mutable état contextuel"

L'utilisation d'un objet de portée locale pour collecter les résultats intermédiaires dans une chaîne de promesses est une approche raisonnable à la question que vous avez posée. Considérez l'extrait suivant:

function getExample(){
    //locally scoped
    const results = {};
    return promiseA(...).then(function(resultA){
        results.a = resultA;
        return promiseB(...);
    }).then(function(resultB){
        results.b = resultB;
        return promiseC(...);
    }).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, donc cette solution utilise une variable localement limitée qui ne cause aucun dommage. Il est uniquement accessible dans la fonction.
  • L'état Mutable est moche, mais cela ne mute pas l'état d'une manière laide. L'état laid de mutable se réfère traditionnellement à la modification de l'état des arguments de fonction ou des variables globales, mais cette approche modifie simplement l'état d'une variable localisée qui existe dans le seul but d'agréger des résultats prometteurs ... 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ésultat, mais cela n'introduit pas de scénario effrayant où l'une des promesses de la chaîne deviendra fausse et sabotera 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 surgira des années plus tard dans la production (à moins !)
  • Cela n'introduit pas de scénario de condition de concurrence résultant d'un appel parallèle, car une nouvelle instance de la variable de résultat est créée pour chaque appel de la fonction getExample.

3
2018-03-24 20:08



Une autre réponse, en utilisant l'exécuteur séquentiel 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
2018-06-10 00:56



Aujourd'hui, j'ai aussi rencontré des questions comme vous. Enfin, je trouve une bonne solution à la question, c'est simple et bon à lire. J'espère que ceci peut vous aider.

Selon how-to-chain-javascript-promesses

ok, regardons le code:

const firstPromise = () => {
    const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('first promise is completed');
            resolve({data: '123'});
        }, 2000);
    });
};

const secondPromise = (someStuff) => {
    const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('second promise is completed');
            resolve({newData: `${someStuff.data} some more data`});
        }, 2000);
    });
};

const thirdPromise = (someStuff) => {
    const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('third promise is completed');
            resolve({result: someStuff});
        }, 2000);
    });
};

firstPromise()
    .then(seondPromise)
    .then(thirdPromise)
    .then(data => {
        console.log(data);
    });

2
2017-07-25 06:34