Che cos'è l '"inferno di richiamata" e come e perché RX lo risolve?


113

Qualcuno può dare una definizione chiara insieme a un semplice esempio che spieghi cos'è un "inferno di richiamata" per qualcuno che non conosce JavaScript e node.js?

Quando (in che tipo di impostazioni) si verifica il "problema dell'inferno di richiamata"?

Perché si verifica?

"Callback hell" è sempre correlato a calcoli asincroni?

O può verificarsi "l'inferno di richiamata" anche in una singola applicazione a thread?

Ho seguito il corso reattivo a Coursera ed Erik Meijer ha detto in una delle sue lezioni che RX risolve il problema del "callback hell". Ho chiesto cos'è un "inferno di richiamata" sul forum di Coursera ma non ho avuto una risposta chiara.

Dopo aver spiegato "callback hell" su un semplice esempio, potresti anche mostrare come RX risolve il "callback hell problem" su quel semplice esempio?

Risposte:


136

1) Cos'è un "callback hell" per qualcuno che non conosce javascript e node.js?

Quest'altra domanda ha alcuni esempi di invenzioni di callback Javascript: come evitare lunghi annidamenti di funzioni asincrone in Node.js

Il problema in Javascript è che l'unico modo per "congelare" un calcolo e fare in modo che il "resto" lo esegua (in modo asincrono) è mettere "il resto" all'interno di un callback.

Ad esempio, diciamo che voglio eseguire un codice simile a questo:

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

Cosa succede se ora desidero rendere asincrone le funzioni getData, il che significa che ho la possibilità di eseguire un altro codice mentre aspetto che restituiscano i loro valori? In Javascript, l'unico modo sarebbe riscrivere tutto ciò che tocca un calcolo asincrono utilizzando lo stile di passaggio di continuazione :

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

Non credo di dover convincere nessuno che questa versione è più brutta della precedente. :-)

2) Quando (in che tipo di impostazioni) si verifica il "problema dell'inferno di richiamata"?

Quando hai molte funzioni di callback nel tuo codice! Diventa più difficile lavorare con loro più ne hai nel tuo codice e diventa particolarmente difficile quando hai bisogno di fare loop, blocchi try-catch e cose del genere.

Ad esempio, per quanto ne so, in JavaScript l'unico modo per eseguire una serie di funzioni asincrone in cui una viene eseguita dopo i ritorni precedenti è utilizzare una funzione ricorsiva. Non puoi usare un ciclo for.

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

Invece, potremmo dover finire per scrivere:

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

//ugh!

Il numero di domande che riceviamo qui su StackOverflow chiedendo come fare questo genere di cose è una testimonianza di quanto sia confuso :)

3) Perché si verifica?

Si verifica perché in JavaScript l'unico modo per ritardare un calcolo in modo che venga eseguito dopo il ritorno della chiamata asincrona è inserire il codice ritardato all'interno di una funzione di callback. Non è possibile ritardare il codice che è stato scritto nel tradizionale stile sincrono, così si finisce con callback annidati ovunque.

4) Oppure può verificarsi "callback hell" anche in una singola applicazione threaded?

La programmazione asincrona ha a che fare con la concorrenza mentre un thread singolo ha a che fare con il parallelismo. I due concetti in realtà non sono la stessa cosa.

È ancora possibile avere codice simultaneo in un singolo contesto a thread. In effetti, JavaScript, la regina dell'inferno di callback, è a thread singolo.

Qual è la differenza tra concorrenza e parallelismo?

5) potresti anche mostrare come RX risolve il "problema dell'inferno di richiamata" su questo semplice esempio.

Non so nulla di RX in particolare, ma di solito questo problema viene risolto aggiungendo il supporto nativo per il calcolo asincrono nel linguaggio di programmazione. Le implementazioni possono variare e includere: async, generators, coroutines e callcc.

In Python possiamo implementare quell'esempio di ciclo precedente con qualcosa sulla falsariga di:

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

myGen = myLoop()

Questo non è il codice completo, ma l'idea è che "yield" interrompa il nostro ciclo for fino a quando qualcuno chiama myGen.next (). La cosa importante è che potremmo ancora scrivere il codice usando un ciclo for, senza bisogno di trasformare la logica "dentro e fuori" come dovevamo fare in quella loopfunzione ricorsiva .


Quindi l'inferno di richiamata può verificarsi solo in un'impostazione asincrona? Se il mio codice è completamente sincrono (cioè nessuna concorrenza), il "callback hell" non può verificarsi se capisco correttamente la tua risposta, è vero?
jhegedus

L'inferno di richiamata ha più a che fare con quanto sia fastidioso scrivere codice usando lo stile di passaggio di continuazione. In teoria potresti ancora riscrivere tutte le tue funzioni usando lo stile CPS anche per un normale programma (l'articolo di wikipedia ha alcuni esempi) ma, per una buona ragione, la maggior parte delle persone non lo fa. Di solito usiamo solo lo stile di passaggio di continuazione se siamo costretti a farlo, come nel caso della programmazione asincrona Javascript.
hugomg

btw, ho cercato su Google le estensioni reattive e ho l'impressione che siano più simili a una libreria Promise e non a un'estensione del linguaggio che introduce la sintassi asincrona. Le promesse aiutano a gestire l'annidamento delle richiamate e la gestione delle eccezioni, ma non sono chiare come le estensioni di sintassi. Il ciclo for è ancora fastidioso da codificare e devi ancora tradurre il codice dallo stile sincrono allo stile della promessa.
hugomg

1
Dovrei chiarire come RX generalmente fa un lavoro migliore. RX è dichiarativo. È possibile dichiarare come il programma risponderà agli eventi quando si verificheranno in seguito senza influire su nessun'altra logica del programma. Ciò consente di separare il codice del loop principale dal codice di gestione degli eventi. Puoi gestire facilmente dettagli come l'ordinamento asincrono degli eventi che sono un incubo quando si utilizzano variabili di stato. Ho scoperto che RX è stata l'implementazione più pulita per eseguire una nuova richiesta di rete dopo che sono state restituite 3 risposte di rete o per gestire l'errore dell'intera catena se non viene restituita. Quindi può resettarsi e attendere gli stessi 3 eventi.
colintheshots l'

Un altro commento correlato: RX è fondamentalmente la monade di continuazione, che si riferisce a CPS se non mi sbaglio, questo potrebbe anche spiegare come / perché RX è buono per il problema di callback / inferno.
jhegedus

30

Rispondi alla domanda: potresti anche mostrare come RX risolve il "problema dell'inferno di richiamata" su quel semplice esempio?

La magia è flatMap. Possiamo scrivere il seguente codice in Rx per l'esempio di @ hugomg:

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

È come se stessi scrivendo alcuni codici FP sincroni, ma in realtà puoi renderli asincroni da Scheduler.


26

Per rispondere alla domanda su come Rx risolve l' inferno di callback :

Per prima cosa descriviamo di nuovo l'inferno di callback.

Immagina un caso in cui dobbiamo fare http per ottenere tre risorse: persona, pianeta e galassia. Il nostro obiettivo è trovare la galassia in cui vive la persona. Prima dobbiamo prendere la persona, poi il pianeta, poi la galassia. Sono tre callback per tre operazioni asincrone.

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

Ciascun callback è annidato. Ciascun callback interno dipende dal suo genitore. Questo porta allo stile "piramide del destino" dell'inferno di richiamo . Il codice ha l'aspetto di un segno>.

Per risolvere questo problema in RxJs potresti fare qualcosa del genere:

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

Con l' operatore mergeMapAKA flatMappotresti renderlo più succinto:

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

Come puoi vedere, il codice è appiattito e contiene un'unica catena di chiamate al metodo. Non abbiamo una "piramide del destino".

Quindi, l'inferno di richiamata viene evitato.

Nel caso ve lo steste chiedendo, le promesse sono un altro modo per evitare l'inferno di richiamata, ma le promesse sono impazienti , non pigre come osservabili e (in generale) non potete cancellarle facilmente.


Non sono uno sviluppatore JS, ma questa è una spiegazione semplice
Omar Beshary

15

L'inferno di richiamata è qualsiasi codice in cui l'uso di richiamate di funzione nel codice asincrono diventa oscuro o difficile da seguire. In genere, quando è presente più di un livello di riferimento indiretto, il codice che utilizza i callback può diventare più difficile da seguire, più difficile da refactoring e più difficile da testare. Un odore di codice è costituito da più livelli di rientro dovuti al passaggio di più livelli di valori letterali di funzione.

Questo accade spesso quando il comportamento ha delle dipendenze, cioè quando A deve accadere prima che B debba accadere prima di C. Quindi ottieni un codice come questo:

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

Se hai molte dipendenze comportamentali nel tuo codice come questo, può diventare fastidioso velocemente. Soprattutto se si ramifica ...

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

Questo non funzionerà. Come possiamo fare in modo che il codice asincrono venga eseguito in un determinato ordine senza dover passare tutti questi callback?

RX è l'abbreviazione di "estensioni reattive". Non l'ho usato, ma Google suggerisce che è un framework basato su eventi, il che ha senso. Gli eventi sono un modello comune per eseguire il codice in ordine senza creare accoppiamenti fragili . Puoi fare in modo che C ascolti l'evento "bFinished" che si verifica solo dopo che B viene chiamato in ascolto di "aFinished". È quindi possibile aggiungere facilmente passaggi aggiuntivi o estendere questo tipo di comportamento e verificare facilmente che il codice venga eseguito in ordine semplicemente trasmettendo eventi nel test case.


1

Call back hell significa che sei all'interno di un callback o di un altro callback e va all'ennesima chiamata finché le tue esigenze non vengono soddisfatte.

Comprendiamo attraverso un esempio di falsa chiamata ajax utilizzando l'API set timeout, supponiamo di avere un'API di ricetta, dobbiamo scaricare tutta la ricetta.

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

Nell'esempio sopra, dopo 1,5 sec quando il timer scade all'interno del codice di richiamata verrà eseguito, in altre parole, tramite la nostra falsa chiamata ajax tutte le ricette verranno scaricate dal server. Ora dobbiamo scaricare i dati di una ricetta particolare.

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

Per scaricare i dati di una ricetta particolare, abbiamo scritto il codice all'interno della nostra prima richiamata e abbiamo passato l'ID della ricetta.

Supponiamo ora di dover scaricare tutte le ricette dello stesso editore della ricetta il cui ID è 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>

Per soddisfare le nostre esigenze, ovvero scaricare tutte le ricette del nome dell'editore suru, abbiamo scritto il codice all'interno della nostra seconda call back. È chiaro che abbiamo scritto una catena di callback che si chiama callback hell.

Se vuoi evitare l'inferno di richiamata, puoi usare Promise, che è la funzione js es6, ogni promessa riceve una richiamata che viene chiamata quando una promessa è piena. promise callback ha due opzioni o è risolto o rifiuta. Supponiamo che la tua chiamata API abbia esito positivo, puoi chiamare la risoluzione e passare i dati attraverso la risoluzione , puoi ottenere questi dati usando then () . Ma se la tua API non è riuscita, puoi usare rifiuta, usa catch per catturare l'errore. Ricordate che una promessa utilizzare sempre poi per determinazione e cattura per respingere

Risolviamo il precedente problema dell'inferno di richiamata usando una promessa.

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

Ora scarica la ricetta particolare:

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

Ora possiamo scrivere un altro metodo chiamato allRecipeOfAPublisher come getRecipe che restituirà anche una promessa, e possiamo scrivere un altro then () per ricevere la promessa di risoluzione per allRecipeOfAPublisher, spero che a questo punto tu possa farlo da solo.

Quindi abbiamo imparato come costruire e consumare promesse, ora rendiamo più facile consumare una promessa usando async / await che è introdotto in 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>

Nell'esempio sopra, abbiamo usato una funzione asincrona perché verrà eseguita in background, all'interno della funzione asincrona abbiamo usato la parola chiave await prima di ogni metodo che ritorna o è una promessa perché aspettare in quella posizione fino a quando quella promessa non viene soddisfatta, in altre parole nel i codici seguenti fino a quando getIds non saranno stati risolti o il programma di rifiuto interromperà l'esecuzione dei codici sotto quella riga quando gli ID sono stati restituiti, quindi abbiamo chiamato di nuovo la funzione getRecipe () con un ID e abbiamo aspettato utilizzando la parola chiave await finché i dati non sono stati restituiti. Quindi è così che finalmente ci siamo ripresi dall'inferno del callback.

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

Per usare await avremo bisogno di una funzione asincrona, possiamo restituire una promessa quindi usa then per risolvere promessa e cath per rifiutare promessa

dall'esempio sopra:

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

0

Un modo in cui si può evitare l'inferno di Callback è usare FRP, che è una "versione avanzata" di RX.

Ho iniziato a usare FRP di recente perché ho trovato una buona implementazione chiamata Sodium( http://sodium.nz/ ).

Un codice tipico è simile a questo (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()è un Streamche si attiva se selectedNode(che è a Cell) cambia, NodeEditorWidgetallora si aggiorna di conseguenza.

Quindi, a seconda del contenuto di selectedNode Cell, quello attualmente modificato Notecambierà.

Questo codice evita completamente i callback, quasi i Cacllback vengono inviati al "livello esterno" / "superficie" dell'app, dove la logica di gestione dello stato si interfaccia con il mondo esterno. Non sono necessari Callback per propagare i dati all'interno della logica di gestione dello stato interna (che implementa una macchina a stati).

Il codice sorgente completo è qui

Lo snippet di codice sopra corrisponde al seguente semplice esempio di creazione / visualizzazione / aggiornamento:

inserisci qui la descrizione dell'immagine

Questo codice invia anche gli aggiornamenti al server, quindi le modifiche alle Entità aggiornate vengono salvate automaticamente sul server.

Tutta la gestione dell'evento è curata utilizzando Streams e Cells. Questi sono concetti FRP. I callback sono necessari solo quando la logica FRP si interfaccia con il mondo esterno, come l'input dell'utente, la modifica del testo, la pressione di un pulsante, la chiamata AJAX ritorna.

Il flusso di dati è descritto in modo esplicito, in modo dichiarativo utilizzando FRP (implementato dalla libreria Sodium), quindi non è necessaria alcuna logica di gestione / callback degli eventi per descrivere il flusso di dati.

FRP (che è una versione più "rigorosa" di RX) è un modo per descrivere un grafico del flusso di dati, che può contenere nodi che contengono lo stato. Gli eventi attivano i cambiamenti di stato nello stato contenente i nodi (chiamati Cells).

Il sodio è una libreria FRP di ordine superiore, il che significa che utilizzando la flatMap/ switchprimitiva è possibile riorganizzare il grafico del flusso di dati in fase di esecuzione.

Consiglio di dare un'occhiata al libro Sodium , spiega in dettaglio come FRP elimina tutti i Callback che non sono essenziali per descrivere la logica del flusso di dati che ha a che fare con l'aggiornamento dello stato delle applicazioni in risposta ad alcuni stimoli esterni.

Utilizzando FRP, è necessario mantenere solo quei Callback che descrivono l'interazione con il mondo esterno. In altre parole, il flusso di dati è descritto in modo funzionale / dichiarativo quando si utilizza un framework FRP (come Sodium), o quando si utilizza un framework "FRP like" (come RX).

Il sodio è disponibile anche per Javascript / Typescript.


-3

Se non hai una conoscenza di callback e hell callback non ci sono problemi.La prima cosa è quella call back e call back hell.Ad esempio: hell call back è come se possiamo memorizzare una classe all'interno di una classe. su quello annidato in linguaggio C, C ++. Annidato Significa che una classe all'interno di un'altra classe.


La risposta sarà più utile se contiene snippet di codice per mostrare cos'è "Callback hell" e lo stesso snippet di codice con Rx dopo aver rimosso "callback hell"
rafa

-4

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

si semplifica in questo modo:

    // esegue attività sequenziali concatenate
    jj.script ([
        // primo compito
        funzione (successiva) {
            // alla fine di questo processo 'next' punta alla seconda attività ed eseguila 
            callAsyncProcess1 (successivo);
        },
      // secondo compito
      funzione (successiva) {
        // alla fine di questo processo, 'next' punta a questa attività ed eseguila 
        callAsyncProcess2 (successivo);
      },
      // thirt task
      funzione (successiva) {
        // alla fine di questo processo 'successivo' punta a (se presente) 
        callAsyncProcess3 (successivo);
      },
    ]);


considera ultra-compatto come questo github.com/Javanile/Jazz.js/wiki/Script-showcase
cicciodarkast
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.