Les promesses ne sont-elles pas de simples rappels?

430

Je développe JavaScript depuis quelques années et je ne comprends pas du tout les histoires de promesses.

Il semble que tout ce que je fais c'est changer:

api(function(result){
    api2(function(result2){
        api3(function(result3){
             // do work
        });
    });
});

Pour lequel je pourrais utiliser une bibliothèque comme async de toute façon, avec quelque chose comme:

api().then(function(result){
     api2().then(function(result2){
          api3().then(function(result3){
               // do work
          });
     });
});

Ce qui est plus de code et moins lisible. Je n'ai rien gagné ici, ce n'est pas soudainement «plat» comme par magie. Sans parler de devoir convertir les choses en promesses.

Alors, quel est le gros problème des promesses ici?

Benjamin Gruenbaum
la source
11
Sur le sujet : il y a un article vraiment informatif sur les promesses sur Html5Rocks: html5rocks.com/en/tutorials/es6/promises
ComFreek
2
Pour info, la réponse que vous avez acceptée est la même vieille liste des avantages triviaux qui ne sont pas du tout des promesses et ne m'a même pas convaincu d'utiliser des promesses: /. Ce qui m'a convaincu d'utiliser des promesses, c'est l'aspect DSL tel que décrit dans la réponse d'Oscar
Esailija
@Esailija bien, votre parole m'a convaincu. J'ai accepté l'autre réponse bien que je pense que celle de Bergi soulève également de très bons points (et différents).
Benjamin Gruenbaum
@Esailija "Ce qui m'a convaincu d'utiliser des promesses, c'est l'aspect DSL tel que décrit dans la réponse d'Oscar" << Qu'est-ce que "DSL"? et quel est "l'aspect DSL" dont vous parlez?
monsto
1
@monsto: DSL: Domain Specific Language, un langage spécialement conçu pour être utilisé dans un sous-ensemble particulier d'un système (par exemple SQL ou ORM pour parler à la base de données, regex pour trouver des modèles, etc.). Dans ce contexte, le "DSL" est l'API de Promise qui, si vous structurez votre code comme le faisait Oscar, est presque comme du sucre syntaxique qui complète JavaScript pour répondre au contexte particulier des opérations asynchrones. Les promesses créent des idiomes qui les transforment en presque un langage conçu pour permettre au programmeur de saisir plus facilement le flux mental quelque peu insaisissable de ce type de structures.
Michael Ekoka

Réponses:

631

Les promesses ne sont pas des rappels. Une promesse représente le résultat futur d'une opération asynchrone . Bien sûr, en les écrivant comme vous le faites, vous obtenez peu d'avantages. Mais si vous les écrivez de la façon dont ils sont censés être utilisés, vous pouvez écrire du code asynchrone d'une manière qui ressemble au code synchrone et est beaucoup plus facile à suivre:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
});

Certes, pas beaucoup moins de code, mais beaucoup plus lisible.

Mais ce n'est pas la fin. Découvrons les véritables avantages: que se passe-t-il si vous souhaitez vérifier toute erreur dans l'une des étapes? Ce serait l'enfer de le faire avec des rappels, mais avec des promesses, c'est du gâteau:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
});

À peu près la même chose qu'un try { ... } catchbloc.

Encore mieux:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
}).then(function() {
     //do something whether there was an error or not
     //like hiding an spinner if you were performing an AJAX request.
});

Et mieux encore: Et si ces 3 appels api, api2, api3pourraient exécuter simultanément (par exemple , si elles étaient des appels AJAX) mais vous besoin d'attendre les trois? Sans promesses, vous devriez avoir à créer une sorte de compteur. Avec des promesses, en utilisant la notation ES6, c'est un autre morceau de gâteau et assez soigné:

Promise.all([api(), api2(), api3()]).then(function(result) {
    //do work. result is an array contains the values of the three fulfilled promises.
}).catch(function(error) {
    //handle the error. At least one of the promises rejected.
});

J'espère que vous voyez les promesses sous un nouveau jour maintenant.

Oscar Paz
la source
124
Ils n'auraient vraiment pas dû le nommer "Promise". "Future" est au moins 100 fois meilleur.
Pacerier
12
@Pacerier parce que Future n'a pas été entaché par jQuery?
Esailija
5
Modèle alternatif (en fonction de ce qui est souhaité: api (). Puis (api2) .then (api3) .then (doWork); Autrement dit, si les fonctions api2 / api3 prennent en compte la dernière étape et renvoient elles-mêmes de nouvelles promesses, elles peut simplement être enchaîné sans emballage supplémentaire. Autrement dit, ils composent.
Dtipson
1
Et s'il y a des opérations asynchrones dans api2et api3? la dernière .thenne serait-elle appelée qu'une fois ces opérations asynchrones terminées?
NiCk Newman le
8
Pourquoi m'as-tu tagué? Je viens de corriger un peu la grammaire. Je ne suis pas un expert JS. :)
Scott Arciszewski
169

Oui, les promesses sont des rappels asynchrones. Ils ne peuvent rien faire que les rappels ne puissent pas faire, et vous rencontrez les mêmes problèmes avec l'asynchronie qu'avec les rappels simples.

Cependant, les promesses sont plus que de simples rappels. Ils sont une abstraction très puissante, permettent un code fonctionnel plus propre et meilleur avec un passe-partout moins sujet aux erreurs.

Alors quelle est l'idée principale?

Les promesses sont des objets représentant le résultat d'un seul calcul (asynchrone). Ils ne résolvent ce résultat qu'une seule fois. Il y a quelques choses ce que cela signifie:

Les promesses mettent en œuvre un modèle d'observation:

  • Vous n'avez pas besoin de connaître les rappels qui utiliseront la valeur avant la fin de la tâche.
  • Au lieu d'attendre des rappels comme arguments de vos fonctions, vous pouvez facilement returnun objet Promise
  • La promesse stockera la valeur et vous pourrez ajouter un rappel de manière transparente à tout moment. Il sera appelé lorsque le résultat sera disponible. La "transparence" implique que lorsque vous avez une promesse et y ajoutez un rappel, cela ne fait aucune différence pour votre code si le résultat est déjà arrivé - l'API et les contrats sont les mêmes, ce qui simplifie beaucoup la mise en cache / mémoisation.
  • Vous pouvez facilement ajouter plusieurs rappels

Les promesses sont chaînables ( monadiques , si vous voulez ):

  • Si vous devez transformer la valeur que représente une promesse, vous mappez une fonction de transformation sur la promesse et récupérez une nouvelle promesse qui représente le résultat transformé. Vous ne pouvez pas obtenir la valeur de manière synchrone pour l'utiliser d'une manière ou d'une autre, mais vous pouvez facilement lever la transformation dans le contexte de la promesse. Pas de rappels standard.
  • Si vous souhaitez chaîner deux tâches asynchrones, vous pouvez utiliser la .then()méthode. Il faudra un rappel pour être appelé avec le premier résultat et retourne une promesse pour le résultat de la promesse que le rappel renvoie.

Cela semble compliqué? Temps pour un exemple de code.

var p1 = api1(); // returning a promise
var p3 = p1.then(function(api1Result) {
    var p2 = api2(); // returning a promise
    return p2; // The result of p2 …
}); // … becomes the result of p3

// So it does not make a difference whether you write
api1().then(function(api1Result) {
    return api2().then(console.log)
})
// or the flattened version
api1().then(function(api1Result) {
    return api2();
}).then(console.log)

L'aplatissement ne vient pas comme par magie, mais vous pouvez facilement le faire. Pour votre exemple fortement imbriqué, l'équivalent (presque) serait

api1().then(api2).then(api3).then(/* do-work-callback */);

Si voir le code de ces méthodes aide à comprendre, voici une lib de promesse la plus basique en quelques lignes .

Quel est le gros problème des promesses?

L'abstraction Promise permet une bien meilleure composabilité des fonctions. Par exemple, à côté thendu chaînage, la allfonction crée une promesse pour le résultat combiné de plusieurs promesses d'attente parallèle.

Enfin et surtout, les promesses sont livrées avec une gestion intégrée des erreurs. Le résultat du calcul peut être que la promesse est remplie d'une valeur ou qu'elle est rejetée avec une raison. Toutes les fonctions de composition gèrent cela automatiquement et propagent les erreurs dans les chaînes de promesses, de sorte que vous n'avez pas besoin de vous en soucier explicitement partout - contrairement à une implémentation de rappel simple. Au final, vous pouvez ajouter un rappel d'erreur dédié pour toutes les exceptions survenues.

Sans parler de devoir convertir les choses en promesses.

C'est assez banal en fait avec de bonnes bibliothèques de promesses, voir Comment convertir une API de rappel existante en promesses?

Bergi
la source
salut Bergi, auriez-vous quelque chose d'intéressant à ajouter à cette question SO? stackoverflow.com/questions/22724883/…
Sebastien Lorber
1
@Sebastien: Je ne connais pas encore très bien Scala, et je n'ai pu que répéter ce que Benjamin a dit :-)
Bergi
3
Juste une petite remarque: vous ne pouvez pas utiliser .then(console.log), car console.log dépend du contexte de la console. De cette façon, cela provoquera une erreur d'invocation illégale. Utilisez console.log.bind(console)ou x => console.log(x)pour lier le contexte.
Tamas Hegedus
3
@hege_hegedus: Il existe des environnements où les consoleméthodes sont déjà liées. Et bien sûr, j'ai seulement dit que les deux nidifications ont exactement le même comportement, pas qu'elles fonctionneraient :-P
Bergi
1
C'était génial. C'est ce dont j'avais besoin: moins de code et plus d'interprétation. Je vous remercie.
Adam Patterson
21

Outre les réponses déjà établies, avec des fonctions de ES6 Promesses tourner d'un brillant modestement petit nain bleu droit dans une géante rouge. C'est sur le point de s'effondrer dans une supernova:

api().then(result => api2()).then(result2 => api3()).then(result3 => console.log(result3))

Comme l'a souligné oligofren , sans arguments entre les appels API, vous n'avez pas du tout besoin des fonctions d'encapsulation anonyme:

api().then(api2).then(api3).then(r3 => console.log(r3))

Et enfin, si vous voulez atteindre un niveau de trou noir supermassif, des promesses peuvent être attendues:

async function callApis() {
    let api1Result = await api();
    let api2Result = await api2(api1Result);
    let api3Result = await api3(api2Result);

    return api3Result;
}
John Weisz
la source
9
« avec ES6 flèche fonctions promesses tour d'une petite étoile bleue directement dans une géante rouge qui brille modestement est sur le point d'effondrement dans une supernova. » Traduction: La combinaison ES6 flèche fonctions avec des promesses est génial :)
user3344977
3
Cela fait sonner les promesses comme une catastrophe cosmique, ce qui, je pense, n'était pas votre intention.
Michael McGinnis
Si vous n'utilisez pas les arguments dans les apiXméthodes, vous pourriez aussi bien ignorer complètement les fonctions de direction: api().then(api2).then(api3).then(r3 => console.log(r3)).
oligofren
@MichaelMcGinnis - L'impact bénéfique de Promises sur un enfer terne de rappel est comme une supernova qui explose dans un coin sombre de l'espace.
John Weisz
Je sais que vous le pensez poétiquement, mais les promesses sont assez loin de la "supernova". On pense à la violation de la loi monadique ou au manque de support pour des cas d'utilisation plus puissants tels que l'annulation ou le retour de plusieurs valeurs.
Dmitri Zaitsev
15

En plus des réponses impressionnantes ci-dessus, 2 points supplémentaires peuvent être ajoutés:

1. Différence sémantique:

Les promesses peuvent être déjà résolues lors de la création. Cela signifie qu'ils garantissent des conditions plutôt que des événements . S'ils sont déjà résolus, la fonction résolue qui lui est transmise est toujours appelée.

Inversement, les rappels gèrent les événements. Par conséquent, si l'événement qui vous intéresse s'est produit avant l'enregistrement du rappel, le rappel n'est pas appelé.

2. Inversion du contrôle

Les rappels impliquent une inversion de contrôle. Lorsque vous enregistrez une fonction de rappel avec une API, le runtime Javascript stocke la fonction de rappel et l'appelle à partir de la boucle d'événements une fois qu'elle est prête à être exécutée.

Reportez-vous à la boucle d'événement Javascript pour une explication.

Avec Promises , le contrôle réside dans le programme appelant. La méthode .then () peut être appelée à tout moment si nous stockons l'objet promise.

dww
la source
1
Je ne sais pas pourquoi mais cela semble être une meilleure réponse.
radiantshaw
13

En plus des autres réponses, la syntaxe ES2015 se fond parfaitement avec les promesses, réduisant encore plus le code standard:

// Sequentially:
api1()
  .then(r1 => api2(r1))
  .then(r2 => api3(r2))
  .then(r3 => {
      // Done
  });

// Parallel:
Promise.all([
    api1(),
    api2(),
    api3()
]).then(([r1, r2, r3]) => {
    // Done
});
Duncan Luk
la source
5

Les promesses ne sont pas des rappels, les deux sont des idiomes de programmation qui facilitent la programmation asynchrone. L'utilisation d'un style de programmation asynchrone / attente à l'aide de coroutines ou de générateurs qui renvoient des promesses pourrait être considérée comme un troisième idiome de ce type. Une comparaison de ces idiomes à travers différents langages de programmation (y compris Javascript) est ici: https://github.com/KjellSchubert/promise-future-task

Kjell Schubert
la source
5

Non pas du tout.

Les rappels sont simplement des fonctions en JavaScript qui doivent être appelées puis exécutées une fois l'exécution d'une autre fonction terminée. Alors comment ça se passe?

En fait, en JavaScript, les fonctions sont elles-mêmes considérées comme des objets et donc comme tous les autres objets, même les fonctions peuvent être envoyées comme arguments à d'autres fonctions . Le cas d'utilisation le plus courant et le plus générique auquel on puisse penser est la fonction setTimeout () en JavaScript.

Les promesses ne sont rien d'autre qu'une approche beaucoup plus improvisée de la gestion et de la structuration du code asynchrone par rapport à la même chose avec les rappels.

La promesse reçoit deux rappels dans la fonction constructeur: résoudre et rejeter. Ces rappels à l'intérieur des promesses nous fournissent un contrôle précis sur la gestion des erreurs et les cas de réussite. Le rappel de résolution est utilisé lorsque l'exécution de la promesse a réussi et le rappel de rejet est utilisé pour gérer les cas d'erreur.

Ayush Jain
la source
2

Aucune promesse ne se limite aux rappels

exemple Vous pouvez utiliser des promesses natives javascript avec le noeud js

my cloud 9 code link : https://ide.c9.io/adx2803/native-promises-in-node

/**
* Created by dixit-lab on 20/6/16.
*/

var express = require('express');
var request = require('request');   //Simplified HTTP request client.


var app = express();

function promisify(url) {
    return new Promise(function (resolve, reject) {
    request.get(url, function (error, response, body) {
    if (!error && response.statusCode == 200) {
        resolve(body);
    }
    else {
        reject(error);
    }
    })
    });
}

//get all the albums of a user who have posted post 100
app.get('/listAlbums', function (req, res) {
//get the post with post id 100
promisify('http://jsonplaceholder.typicode.com/posts/100').then(function (result) {
var obj = JSON.parse(result);
return promisify('http://jsonplaceholder.typicode.com/users/' + obj.userId + '/albums')
})
.catch(function (e) {
    console.log(e);
})
.then(function (result) {
    res.end(result);
}
)

})


var server = app.listen(8081, function () {

var host = server.address().address
var port = server.address().port

console.log("Example app listening at http://%s:%s", host, port)

})


//run webservice on browser : http://localhost:8081/listAlbums
Apoorv
la source
1

Les promesses JavaScript utilisent en fait des fonctions de rappel pour déterminer quoi faire après la résolution ou le rejet d'une promesse, les deux ne sont donc pas fondamentalement différentes. L'idée principale de Promises est de prendre des rappels - en particulier des rappels imbriqués où vous souhaitez effectuer une sorte d'actions, mais ce serait plus lisible.

Hamid Shoja
la source
0

Aperçu des promesses:

Dans JS, nous pouvons encapsuler des opérations asynchrones (par exemple, appels de base de données, appels AJAX) en promesses. Habituellement, nous voulons exécuter une logique supplémentaire sur les données récupérées. JS promet d'avoir des fonctions de gestionnaire qui traitent le résultat des opérations asynchrones. Les fonctions de gestionnaire peuvent même contenir d'autres opérations asynchrones qui peuvent dépendre de la valeur des opérations asynchrones précédentes.

Une promesse a toujours les 3 états suivants:

  1. en attente: état de départ de chaque promesse, ni respecté ni rejeté.
  2. remplie: l'opération s'est terminée avec succès.
  3. rejeté: l'opération a échoué.

Une promesse en attente peut être résolue / remplie ou rejetée avec une valeur. Ensuite, les méthodes de gestionnaire suivantes qui prennent des rappels comme arguments sont appelées:

  1. Promise.prototype.then() : Lorsque la promesse est résolue, l'argument de rappel de cette fonction sera appelé.
  2. Promise.prototype.catch() : Lorsque la promesse est rejetée, l'argument de rappel de cette fonction sera appelé.

Bien que les compétences de méthodes ci-dessus obtiennent des arguments de rappel, elles sont bien supérieures à l'utilisation de seuls rappels, voici un exemple qui clarifiera beaucoup:

Exemple

function createProm(resolveVal, rejectVal) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() > 0.5) {
                console.log("Resolved");
                resolve(resolveVal);
            } else {
                console.log("Rejected");
                reject(rejectVal);
            }
        }, 1000);
    });
}

createProm(1, 2)
    .then((resVal) => {
        console.log(resVal);
        return resVal + 1;
    })
    .then((resVal) => {
        console.log(resVal);
        return resVal + 2;
    })
    .catch((rejectVal) => {
        console.log(rejectVal);
        return rejectVal + 1;
    })
    .then((resVal) => {
        console.log(resVal);
    })
    .finally(() => {
        console.log("Promise done");
    });

  • La fonction createProm crée une promesse qui est résolue ou rejetée sur la base d'un Nr aléatoire après 1 seconde
  • Si la promesse est résolue, la première thenméthode est appelée et la valeur résolue est transmise en tant qu'argument du rappel
  • Si la promesse est rejetée, la première catchméthode est appelée et la valeur rejetée est transmise en argument
  • Les méthodes catchet thenretournent des promesses, c'est pourquoi nous pouvons les enchaîner. Ils encapsulent toute valeur retournée Promise.resolveet toute valeur renvoyée (à l'aide du throwmot clé) Promise.reject. Donc, toute valeur retournée est transformée en promesse et sur cette promesse, nous pouvons à nouveau appeler une fonction de gestionnaire.
  • Les chaînes de promesse nous offrent un contrôle plus précis et une meilleure vue d'ensemble que les rappels imbriqués. Par exemple, la catchméthode gère toutes les erreurs qui se sont produites avant le catchgestionnaire.
Willem van der Veen
la source