web-dev-qa-db-fra.com

Qu'est-ce que "l'enfer du rappel" et comment et pourquoi RX le résout-il?

Quelqu'un peut-il donner une définition claire avec un exemple simple qui explique ce qu'est un "enfer de rappel" pour quelqu'un qui ne connaît pas JavaScript et node.js?

Quand (dans quel type de paramètres) le "problème d'enfer de rappel" se produit-il?

Pourquoi cela se produit-il?

Est-ce que "callback hell" est toujours lié aux calculs asynchrones?

Ou bien peut-il y avoir "callback enfer" dans une seule application threadée?

J'ai suivi le cours réactif à Coursera et Erik Meijer a déclaré dans l'une de ses conférences que RX résolvait le problème de "l'enfer du rappel". J'ai demandé ce qu'est un "enfer de rappel" sur le forum Coursera mais je n'ai pas eu de réponse claire.

Après avoir expliqué "callback hell" à un exemple simple, pourriez-vous également montrer comment RX résout le "problème de callback Hell" sur cet exemple simple?

97
jhegedus

1) Qu'est-ce qu'un "enfer de rappel" pour quelqu'un qui ne connaît pas javascript et node.js?

Cette autre question a quelques exemples de callback Javascript: Comment éviter une longue imbrication de fonctions asynchrones dans Node.js

Le problème en Javascript est que le seul moyen de "geler" un calcul et de laisser le "reste" s'exécuter plus tard (de manière asynchrone) est de mettre "le reste" à l'intérieur d'un rappel.

Par exemple, disons que je veux exécuter un code qui ressemble à ceci:

x = getData();
y = getMoreData(x);
z = getMoreData(y);
...

Que se passe-t-il si je souhaite maintenant rendre les fonctions getData asynchrones, ce qui signifie que je peux exécuter un autre code en attendant que leurs valeurs soient renvoyées? En Javascript, le seul moyen serait de réécrire tout ce qui concerne un calcul asynchrone en utilisant style de passage contin :

getData(function(x){
    getMoreData(x, function(y){
        getMoreData(y, function(z){ 
            ...
        });
    });
});

Je ne pense pas avoir besoin de convaincre qui que ce soit que cette version est plus laide que la précédente. :-)

2) Quand (dans quel type de paramètres) le "problème d'enfer de rappel" se produit-il?

Lorsque vous avez beaucoup de fonctions de rappel dans votre code! Il est de plus en plus difficile de travailler avec eux, d'autant plus que votre code en contient. Cela devient particulièrement pénalisant lorsque vous devez créer des boucles, des blocs try-catch, etc.

Par exemple, pour autant que je sache, la seule façon d'exécuter une série de fonctions asynchrones en JavaScript après les retours précédents consiste à utiliser une fonction récursive. Vous ne pouvez pas utiliser de boucle for.

// we would like to write the following
for(var i=0; i<10; i++){
    doSomething(i);
}
blah();

Au lieu de cela, nous pourrions avoir besoin de finir par écrire:

function loop(i, onDone){
    if(i >= 10){
        onDone()
    }else{
        doSomething(i, function(){
            loop(i+1, onDone);
        });
     }
}
loop(0, function(){
    blah();
});

//ugh!

Le nombre de questions que nous avons ici sur StackOverflow et demandant comment faire ce genre de chose est un témoignage de la confusion qui règne :)

3) Pourquoi cela se produit-il?

Cela se produit car, dans JavaScript, le seul moyen de retarder un calcul pour qu'il s'exécute après le retour de l'appel asynchrone est de placer le code retardé dans une fonction de rappel. Vous ne pouvez pas retarder le code écrit dans un style synchrone traditionnel, vous vous retrouvez donc partout avec des rappels imbriqués.

4) Ou peut-être "callback enfer" se produire également dans une application unique threaded?

La programmation asynchrone a à voir avec la simultanéité, tandis qu'un thread simple a le parallélisme. Les deux concepts ne sont en réalité pas la même chose.

Vous pouvez toujours avoir du code simultané dans un seul contexte threadé. En fait, JavaScript, la reine de l'enfer du rappel, est un seul thread.

Quelle est la différence entre la simultanéité et le parallélisme?

5) pourriez-vous s'il vous plaît également montrer comment RX résout le "problème d'enfer de rappel" sur cet exemple simple.

Je ne connais rien au sujet de RX en particulier, mais ce problème est généralement résolu en ajoutant la prise en charge native du calcul asynchrone dans le langage de programmation. Les implémentations peuvent varier et inclure: async, générateurs, coroutines et callcc.

Dans Python, nous pouvons implémenter cet exemple de boucle précédent avec quelque chose du type:

def myLoop():
    for i in range(10):
        doSomething(i)
        yield

myGen = myLoop()

Ce n'est pas le code complet mais l'idée est que le "rendement" met en pause notre boucle for jusqu'à ce que quelqu'un appelle myGen.next (). L'important est que nous puissions toujours écrire le code en utilisant une boucle for, sans avoir à transformer la logique "à l'envers" comme nous devions le faire dans cette fonction récursive loop.

115
hugomg

Répondez simplement à la question: pourriez-vous s'il vous plaît également montrer comment RX résout le "problème d'enfer de rappel" sur cet exemple simple?

La magie c'est flatMap. Nous pouvons écrire le code suivant dans Rx pour l'exemple de @ hugomg:

def getData() = Observable[X]
getData().flatMap(x -> Observable[Y])
         .flatMap(y -> Observable[Z])
         .map(z -> ...)...

C'est comme si vous écriviez des codes FP) synchrones, mais en réalité, vous pouvez les rendre asynchrones par Scheduler.

29
zsxwing

Pour répondre à la question de savoir comment Rx résout callback enfer:

Commençons par décrire l'enfer de rappel à nouveau.

Imaginons un cas où nous devons utiliser http pour obtenir trois ressources: personne, planète et galaxie. Notre objectif est de trouver la galaxie dans laquelle vit la personne. Nous devons d'abord obtenir la personne, puis la planète, puis la galaxie. C'est trois callbacks pour trois opérations asynchrones.

getPerson(person => { 
   getPlanet(person, (planet) => {
       getGalaxy(planet, (galaxy) => {
           console.log(galaxy);
       });
   });
});

Chaque rappel est imbriqué. Chaque rappel interne dépend de son parent. Cela conduit au style "pyramide de Doom" de enfer de rappel. Le code ressemble à un signe>.

Pour résoudre ceci dans RxJs, vous pouvez faire quelque chose comme ceci:

getPerson()
  .map(person => getPlanet(person))
  .map(planet => getGalaxy(planet))
  .mergeAll()
  .subscribe(galaxy => console.log(galaxy));

Avec l'opérateur mergeMap AKA flatMap, vous pouvez le rendre plus succinct:

getPerson()
  .mergeMap(person => getPlanet(person))
  .mergeMap(planet => getGalaxy(planet))
  .subscribe(galaxy => console.log(galaxy));

Comme vous pouvez le constater, le code est aplati et contient une seule chaîne d'appels de méthode. Nous n'avons pas de "pyramide de Doom".

Par conséquent, l'enfer de rappel est évité.

Au cas où vous vous le demandiez, promesses sont une autre façon d'éviter l'enfer de rappel, mais les promesses sont impatient, pas paresseux comme observables et (généralement parlant) vous ne pouvez pas les annuler aussi facilement.

20
ghostypants

Callback Hell est tout code dans lequel l'utilisation des rappels de fonction dans le code asynchrone devient obscure ou difficile à suivre. En règle générale, lorsqu'il existe plus d'un niveau d'indirection, le code utilisant des rappels peut devenir plus difficile à suivre, plus difficile à refactoriser, et plus difficile à tester. Une odeur de code correspond à plusieurs niveaux d'indentation dus au passage de plusieurs couches de littéraux de fonction.

Cela se produit souvent lorsque le comportement a des dépendances, c’est-à-dire quand A doit avoir lieu avant que B soit avant C.

a({
    parameter : someParameter,
    callback : function() {
        b({
             parameter : someOtherParameter,
             callback : function({
                 c(yetAnotherParameter)
        })
    }
});

Si votre code comporte de nombreuses dépendances comportementales, cela peut devenir gênant rapidement. Surtout s'il y a des branches ...

a({
    parameter : someParameter,
    callback : function(status) {
        if (status == states.SUCCESS) {
          b(function(status) {
              if (status == states.SUCCESS) {
                 c(function(status){
                     if (status == states.SUCCESS) {
                         // Not an exaggeration. I have seen
                         // code that looks like this regularly.
                     }
                 });
              }
          });
        } elseif (status == states.PENDING {
          ...
        }
    }
});

Cela ne va pas. Comment pouvons-nous faire exécuter du code asynchrone dans un ordre déterminé sans avoir à transmettre tous ces rappels?

RX est l'abréviation de 'réactive extensions'. Je ne l'ai pas utilisé, mais Google suggère que c'est un cadre basé sur des événements, ce qui est logique. Les événements sont un modèle courant pour que le code s'exécute dans l'ordre sans créer de couplage fragile . Vous pouvez faire en sorte que C écoute l'événement 'bFinished' qui ne se produit qu'après l'appel de B en train d'écouter 'aFinished'. Vous pouvez ensuite facilement ajouter des étapes supplémentaires ou étendre ce type de comportement, et pouvez vérifier facilement que votre code s'exécute dans l'ordre en ne diffusant que les événements de votre scénario de test. .

14
Jimmy Breck-McKye