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

113

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 de l'enfer du rappel" se produit-il?

Pourquoi cela se produit-il?

L '"enfer de rappel" est-il toujours lié aux calculs asynchrones?

Ou "l'enfer de rappel" peut-il se produire également 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ésout 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 obtenu de réponse claire.

Après avoir expliqué "l'enfer du rappel" sur un exemple simple, pourriez-vous également montrer comment RX résout le "problème de l'enfer du rappel" sur cet exemple simple?

jhegedus
la source

Réponses:

136

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 l'enfer des callbacks Javascript: Comment éviter l'imbrication longue de fonctions asynchrones dans Node.js

Le problème en Javascript est que la seule façon de "figer" un calcul et de faire exécuter le "reste" (de manière asynchrone) est de mettre "le reste" dans un callback.

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

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

Que se passe-t-il si maintenant je veux rendre les fonctions getData asynchrones, ce qui signifie que j'ai la possibilité d'exécuter un autre code pendant que j'attends qu'ils renvoient leurs valeurs? En Javascript, le seul moyen serait de réécrire tout ce qui touche un calcul asynchrone en utilisant le style de passage de continuation :

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 moche que la précédente. :-)

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

Lorsque vous avez beaucoup de fonctions de rappel dans votre code! Il devient de plus en plus difficile de travailler avec eux plus vous en avez dans votre code et cela devient particulièrement difficile lorsque vous avez besoin de faire des boucles, des blocs try-catch et des choses comme ça.

Par exemple, pour autant que je sache, en JavaScript, le seul moyen d'exécuter une série de fonctions asynchrones où l'une est exécutée après les retours précédents consiste à utiliser une fonction récursive. Vous ne pouvez pas utiliser une 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 recevons ici sur StackOverflow demandant comment faire ce genre de chose témoigne de la confusion :)

3) Pourquoi cela se produit-il?

Cela se produit parce qu'en JavaScript, la seule façon de retarder un calcul afin 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 qui a été écrit dans un style synchrone traditionnel afin de vous retrouver avec des rappels imbriqués partout.

4) Ou "l'enfer de rappel" peut-il se produire également dans une seule application threadée?

La programmation asynchrone a à voir avec la concurrence tandis qu'un seul thread a à voir avec le parallélisme. Les deux concepts ne sont en fait pas la même chose.

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

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

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

Je ne sais rien de RX en particulier, mais ce problème est généralement résolu en ajoutant un support natif pour le calcul asynchrone dans le langage de programmation. Les implémentations peuvent varier et inclure: async, générateurs, coroutines et callcc.

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

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 "yield" met en pause notre boucle for jusqu'à ce que quelqu'un appelle myGen.next (). L'important est que nous pourrions toujours écrire le code en utilisant une boucle for, sans avoir besoin de retourner la logique «à l'envers» comme nous avons dû le faire dans cette loopfonction récursive .

hugomg
la source
L'enfer de rappel ne peut donc se produire que dans un cadre asynchrone? Si mon code est entièrement synchrone (c'est-à-dire pas de concurrence), alors "l'enfer de rappel" ne peut pas se produire si je comprends bien votre réponse, n'est-ce pas?
jhegedus
L'enfer des rappels a plus à voir avec la façon dont il est ennuyeux de coder en utilisant le style de passage continu. Théoriquement, vous pouvez toujours réécrire toutes vos fonctions en utilisant le style CPS même pour un programme régulier (l'article de wikipedia a quelques exemples) mais, pour une bonne raison, la plupart des gens ne le font pas. Habituellement, nous n'utilisons le style de passage de continuation que si nous y sommes forcés, ce qui est le cas pour la programmation asynchrone Javascript.
hugomg
btw, j'ai cherché les extensions réactives sur Google et j'ai l'impression qu'elles ressemblent plus à une bibliothèque Promise et non à une extension de langage introduisant la syntaxe asynchrone. Les promesses aident à gérer l'imbrication des rappels et la gestion des exceptions, mais elles ne sont pas aussi soignées que les extensions de syntaxe. La boucle for est toujours ennuyeuse pour le code et vous devez toujours traduire le code du style synchrone au style promis.
hugomg
1
Je devrais clarifier comment RX fait généralement un meilleur travail. RX est déclaratif. Vous pouvez déclarer comment le programme répondra aux événements lorsqu'ils se produiront ultérieurement sans affecter aucune autre logique du programme. Cela vous permet de séparer le code de la boucle principale du code de gestion des événements. Vous pouvez facilement gérer des détails comme l'ordre des événements asynchrones qui sont un cauchemar lors de l'utilisation de variables d'état. J'ai trouvé que RX était la mise en œuvre la plus propre pour effectuer une nouvelle demande réseau après le retour de 3 réponses réseau ou pour gérer l'erreur toute la chaîne si l'une ne retourne pas. Ensuite, il peut se réinitialiser et attendre les 3 mêmes événements.
colintheshots
Un autre commentaire connexe: RX est fondamentalement la monade de continuation, qui se rapporte à CPS si je ne me trompe pas, cela pourrait également expliquer comment / pourquoi RX est bon pour le problème de rappel / enfer.
jhegedus
30

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

La magie 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 fait, vous pouvez les rendre asynchrones par Scheduler.

zsxwing
la source
26

Pour répondre à la question de savoir comment Rx résout l' enfer des rappels :

Décrivons d'abord l'enfer des rappels.

Imaginez un cas où nous devons faire 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. Cela représente trois rappels 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 du destin" de l' enfer de rappel . Le code ressemble à un signe>.

Pour résoudre ce problème 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 mergeMapAKA 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 voir, le code est aplati et contient une seule chaîne d'appels de méthode. Nous n'avons pas de "pyramide de malheur".

Par conséquent, l'enfer des rappels est évité.

Au cas où vous vous poseriez la question, les promesses sont un autre moyen d'éviter l'enfer des rappels, mais les promesses sont avides , pas paresseuses comme les observables et (en général) vous ne pouvez pas les annuler aussi facilement.

fantômes
la source
Je ne suis pas un développeur JS, mais c'est une explication facile
Omar Beshary
15

L'enfer des rappels est tout code où l'utilisation des rappels de fonction dans le code asynchrone devient obscure ou difficile à suivre. Généralement, lorsqu'il y a plus d'un niveau d'indirection, le code utilisant des callbacks 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 lorsque A doit se produire avant que B ne se produise avant C. Ensuite, vous obtenez un code comme celui-ci:

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

Si vous avez beaucoup de dépendances comportementales dans votre code comme celle-ci, cela peut devenir rapidement gênant. Surtout si ça se ramifie ...

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 {
          ...
        }
    }
});

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

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

Jimmy Breck-McKye
la source
1

L'enfer de rappel signifie que vous êtes à l'intérieur d'un rappel d'un autre rappel et qu'il passe au nième appel jusqu'à ce que vos besoins ne soient pas satisfaits.

Comprenons à travers un exemple de faux appel ajax en utilisant l'API set timeout, supposons que nous ayons une API de recette, nous devons télécharger toutes les recettes.

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
            }, 1500);
        }
        getRecipe();
    </script>
</body>

Dans l'exemple ci-dessus, après 1,5 seconde, lorsque la minuterie expire, le code de rappel sera exécuté, en d'autres termes, grâce à notre faux appel ajax, toutes les recettes seront téléchargées depuis le serveur. Nous devons maintenant télécharger les données d'une recette particulière.

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

Pour télécharger une donnée de recette particulière, nous avons écrit du code à l'intérieur de notre premier rappel et passé l'ID de recette.

Disons maintenant que nous devons télécharger toutes les recettes du même éditeur de la recette dont l'ID est 7638.

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                    setTimeout(publisher=>{
                        const recipe2 = {title:'Fresh Apple Pie', publisher:'Suru'};
                        console.log(recipe2);
                    }, 1500, recipe.publisher);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

Pour répondre à tous nos besoins, c'est-à-dire télécharger toutes les recettes du nom d'éditeur suru, nous avons écrit du code à l'intérieur de notre deuxième rappel. Il est clair que nous avons écrit une chaîne de callback qui s'appelle Callback Hell.

Si vous voulez éviter l'enfer des rappels, vous pouvez utiliser Promise, qui est la fonctionnalité js es6, chaque promesse prend un rappel qui est appelé lorsqu'une promesse est remplie. Le rappel de promesse a deux options: il est résolu ou rejeté. Supposons que votre appel API aboutisse, vous pouvez appeler la résolution et transmettre les données via la résolution , vous pouvez obtenir ces données en utilisant then () . Mais si votre API échoue, vous pouvez utiliser le rejet, utiliser catch pour attraper l'erreur. Rappelez - vous une promesse de toujours utiliser alors pour résoudre et prises pour rejeter

Résolvons le problème précédent de l'enfer des rappels en utilisant une promesse.

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        getIds.then(IDs=>{
            console.log(IDs);
        }).catch(error=>{
            console.log(error);
        });
    </script>
</body>

Maintenant, téléchargez une recette particulière:

<body>
    <script>
        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }
        getIds.then(IDs=>{
            console.log(IDs);
            return getRecipe(IDs[2]);
        }).
        then(recipe =>{
            console.log(recipe);
        })
        .catch(error=>{
            console.log(error);
        });
    </script>
</body>

Maintenant, nous pouvons écrire une autre méthode appelée allRecipeOfAPublisher comme getRecipe qui renverra également une promesse, et nous pouvons en écrire une autre then () pour recevoir la promesse de résolution pour allRecipeOfAPublisher, j'espère qu'à ce stade, vous pourrez le faire vous-même.

Nous avons donc appris à construire et à consommer des promesses, facilitons maintenant la consommation d'une promesse en utilisant async / await qui est introduit dans es8.

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }

        async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

        getRecipesAw();
    </script>
</body>

Dans l'exemple ci-dessus, nous avons utilisé une fonction async car elle s'exécutera en arrière-plan, à l'intérieur de la fonction async nous avons utilisé le mot-clé await avant chaque méthode qui retourne ou est une promesse car attendre sur cette position jusqu'à ce que cette promesse se réalise, en d'autres termes dans le les codes ci-dessous jusqu'à ce que getIds soit résolu ou le programme de rejet cessera d'exécuter les codes sous cette ligne lorsque les ID sont retournés, puis nous avons à nouveau appelé la fonction getRecipe () avec un identifiant et avons attendu en utilisant le mot-clé await jusqu'à ce que les données soient renvoyées. C'est ainsi que nous avons finalement récupéré de l'enfer du rappel.

  async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

Pour utiliser await, nous aurons besoin d'une fonction asynchrone, nous pouvons renvoyer une promesse, alors utilisez-la pour résoudre la promesse et cath pour rejeter la promesse

à partir de l'exemple ci-dessus:

 async function getRecipesAw(){
            const IDs = await getIds;
            const recipe = await getRecipe(IDs[2]);
            return recipe;
        }

        getRecipesAw().then(result=>{
            console.log(result);
        }).catch(error=>{
            console.log(error);
        });
Seigneur
la source
0

Une façon d'éviter l'enfer du rappel est d'utiliser FRP qui est une «version améliorée» de RX.

J'ai commencé à utiliser FRP récemment car j'en ai trouvé une bonne implémentation appelée Sodium( http://sodium.nz/ ).

Un code typique ressemble à ceci (Scala.js):

def render: Unit => VdomElement = { _ =>
  <.div(
    <.hr,
    <.h2("Note Selector"),
    <.hr,
    <.br,
    noteSelectorTable.comp(),
    NoteCreatorWidget().createNewNoteButton.comp(),
    NoteEditorWidget(selectedNote.updates()).comp(),
    <.hr,
    <.br
  )
}

selectedNote.updates()est un Streamqui se déclenche si selectedNode(qui est a Cell) change, NodeEditorWidgetalors se met à jour en conséquence.

Ainsi, en fonction du contenu du selectedNode Cell, celui actuellement édité Notechangera.

Ce code évite que les Callback-s entièrement, presque, les Cacllback-s soient poussés vers la "couche externe" / "surface" de l'application, où la logique de gestion d'état s'interface avec le monde externe. Aucun rappel n'est nécessaire pour propager les données dans la logique de gestion d'état interne (qui implémente une machine d'état).

Le code source complet est ici

L'extrait de code ci-dessus correspond à l'exemple de création / affichage / mise à jour simple suivant:

entrez la description de l'image ici

Ce code envoie également des mises à jour au serveur, de sorte que les modifications apportées aux entités mises à jour sont automatiquement enregistrées sur le serveur.

Toute la gestion des événements est prise en charge en utilisant Streams et Cells. Ce sont des concepts FRP. Les rappels ne sont nécessaires que lorsque la logique FRP s'interface avec le monde externe, comme l'entrée utilisateur, l'édition de texte, la pression sur un bouton, l'appel AJAX revient.

Le flux de données est décrit explicitement, de manière déclarative à l'aide de FRP (implémenté par la bibliothèque Sodium), de sorte qu'aucune logique de gestion d'événements / de rappel n'est nécessaire pour décrire le flux de données.

FRP (qui est une version plus "stricte" de RX) est un moyen de décrire un graphe de flux de données, qui peut contenir des nœuds contenant un état. Les événements déclenchent des changements d'état dans l'état contenant des nœuds (appelés Cells).

Sodium est une bibliothèque FRP d'ordre supérieur, ce qui signifie que l'utilisation de la primitive flatMap/ switchpermet de réorganiser le graphique de flux de données au moment de l'exécution.

Je recommande de jeter un coup d'œil dans le livre Sodium , il explique en détail comment FRP se débarrasse de tous les rappels qui ne sont pas essentiels pour décrire la logique de flux de données qui a à voir avec la mise à jour de l'état des applications en réponse à certains stimuli externes.

En utilisant FRP, seuls les rappels doivent être conservés qui décrivent l'interaction avec le monde externe. En d'autres termes, le flux de données est décrit de manière fonctionnelle / déclarative lorsque l'on utilise un framework FRP (tel que Sodium), ou lorsque l'on utilise un framework "FRP like" (tel que RX).

Sodium est également disponible pour Javascript / Typescript.

jhegedus
la source
-3

Si vous ne connaissez pas le callback et le callback de l'enfer, il n'y a pas de problème, le premier est que le rappel et le rappel de l'enfer, par exemple: le rappel de l'enfer est comme si nous pouvons stocker une classe dans une classe. à propos de celui imbriqué en langage C, C ++.Nested Signifie qu'une classe dans une autre classe.

Raghav Arora
la source
La réponse sera plus utile si elle contient un extrait de code pour montrer ce qu'est `` l'enfer de rappel '' et le même extrait de code avec Rx après la suppression de `` l'enfer de rappel ''
rafa
-4

Utilisez jazz.js https://github.com/Javanile/Jazz.js

ça simplifie comme ça:

    // exécuter une tâche séquentielle enchaînée
    jj.script ([
        // première tâche
        function (suivant) {
            // à la fin de ce processus, «suivant», pointez sur la deuxième tâche et exécutez-la 
            callAsyncProcess1 (suivant);
        },
      // deuxième tâche
      function (suivant) {
        // à la fin de ce processus, «suivant», pointez sur la trentième tâche et exécutez-la 
        callAsyncProcess2 (suivant);
      },
      // trentième tâche
      function (suivant) {
        // à la fin de ce processus, pointez vers (si vous l'avez) 
        callAsyncProcess3 (suivant);
      },
    ]);

cicciodarkast
la source
considérez ultra-compact comme celui-ci github.com/Javanile/Jazz.js/wiki/Script-showcase
cicciodarkast