O que é “inferno de retorno de chamada” e como e por que o RX o resolve?

113

Alguém pode dar uma definição clara junto com um exemplo simples que explica o que é um "inferno de callback" para alguém que não conhece JavaScript e node.js?

Quando (em que tipo de configurações) ocorre o "problema de retorno de chamada"?

Por que isso ocorre?

O "inferno de retorno de chamada" está sempre relacionado a cálculos assíncronos?

Ou pode ocorrer "inferno de retorno de chamada" também em um único aplicativo threaded?

Fiz o Curso Reativo no Coursera e Erik Meijer disse em uma de suas palestras que RX resolve o problema do "inferno de callback". Eu perguntei o que é um "inferno de callback" no fórum do Coursera, mas não obtive uma resposta clara.

Depois de explicar "inferno de retorno de chamada" em um exemplo simples, você também poderia mostrar como RX resolve o "problema de inferno de retorno de chamada" nesse exemplo simples?

Jhegedus
fonte

Respostas:

136

1) O que é um "inferno de callback" para alguém que não conhece javascript e node.js?

Esta outra pergunta tem alguns exemplos de inferno de retorno de chamada de Javascript: Como evitar aninhamento longo de funções assíncronas em Node.js

O problema em Javascript é que a única maneira de "congelar" um cálculo e fazer com que o "resto" seja executado posteriormente (de forma assíncrona) é colocar "o resto" dentro de um retorno de chamada.

Por exemplo, digamos que eu queira executar um código parecido com este:

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

O que acontecerá se eu quiser tornar as funções getData assíncronas, o que significa que tenho a chance de executar algum outro código enquanto espero que eles retornem seus valores? Em Javascript, a única maneira seria reescrever tudo o que toca em uma computação assíncrona usando o estilo de passagem de continuação :

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

Acho que não preciso convencer ninguém de que essa versão é mais feia que a anterior. :-)

2) Quando (em que tipo de configurações) ocorre o "problema do callback hell"?

Quando você tem muitas funções de retorno de chamada em seu código! Fica mais difícil trabalhar com eles quanto mais você tem em seu código e fica particularmente ruim quando você precisa fazer loops, blocos try-catch e coisas assim.

Por exemplo, até onde eu sei, em JavaScript, a única maneira de executar uma série de funções assíncronas em que uma é executada após os retornos anteriores é usando uma função recursiva. Você não pode usar um loop for.

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

Em vez disso, talvez precisemos acabar escrevendo:

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

//ugh!

O número de perguntas que recebemos aqui no StackOverflow perguntando como fazer esse tipo de coisa é uma prova de como isso é confuso :)

3) Por que isso ocorre?

Isso ocorre porque no JavaScript a única maneira de atrasar um cálculo para que ele seja executado após o retorno da chamada assíncrona é colocar o código atrasado dentro de uma função de retorno de chamada. Você não pode atrasar o código que foi escrito no estilo síncrono tradicional, então você acaba com callbacks aninhados em todos os lugares.

4) Ou o "inferno de callback" pode ocorrer também em um único aplicativo threaded?

A programação assíncrona tem a ver com simultaneidade, enquanto um thread único tem a ver com paralelismo. Na verdade, os dois conceitos não são a mesma coisa.

Você ainda pode ter código simultâneo em um único contexto de thread. Na verdade, JavaScript, a rainha do inferno de callbacks, é de thread único.

Qual é a diferença entre simultaneidade e paralelismo?

5) você poderia, por favor, mostrar também como o RX resolve o "problema do callback hell" naquele exemplo simples.

Não sei nada sobre RX em particular, mas normalmente esse problema é resolvido adicionando suporte nativo para computação assíncrona na linguagem de programação. As implementações podem variar e incluir: assíncrono, geradores, corrotinas e callcc.

Em Python, podemos implementar esse exemplo de loop anterior com algo ao longo das linhas de:

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

myGen = myLoop()

Este não é o código completo, mas a ideia é que o "rendimento" pause nosso loop for até que alguém chame myGen.next (). O importante é que ainda podemos escrever o código usando um loop for, sem precisar transformar a lógica "de dentro para fora" como fizemos naquela loopfunção recursiva .

hugomg
fonte
Portanto, o inferno de retorno de chamada só pode ocorrer em uma configuração assíncrona? Se meu código for totalmente síncrono (ou seja, sem simultaneidade), então o "inferno de retorno de chamada" não pode ocorrer se eu entendi sua resposta corretamente, certo?
jhegedus
O inferno de callback tem mais a ver com o quão chato é codificar usando o estilo de passagem de continuação. Teoricamente, você ainda poderia reescrever todas as suas funções usando o estilo CPS, mesmo para um programa normal (o artigo da Wikipedia tem alguns exemplos), mas, por boas razões, a maioria das pessoas não o faz. Normalmente, só usamos o estilo de passagem de continuação se formos obrigados, o que é o caso da programação assíncrona Javascript.
hugomg
btw, eu pesquisei as extensões reativas e estou tendo a impressão de que são mais semelhantes a uma biblioteca Promise e não uma extensão de linguagem que introduz sintaxe assíncrona. As promessas ajudam a lidar com o aninhamento de retorno de chamada e com o tratamento de exceções, mas não são tão simples quanto as extensões de sintaxe. O loop for ainda é incômodo para o código e você ainda precisa traduzir o código do estilo síncrono para o estilo promessa.
hugomg
1
Devo esclarecer como RX geralmente faz um trabalho melhor. RX é declarativo. Você pode declarar como o programa responderá aos eventos quando eles ocorrerem posteriormente sem afetar qualquer outra lógica do programa. Isso permite que você separe o código do loop principal do código de tratamento de eventos. Você pode lidar facilmente com detalhes como a ordenação de eventos assíncronos que são um pesadelo ao usar variáveis ​​de estado. Descobri que RX era a implementação mais limpa para realizar uma nova solicitação de rede após o retorno de 3 respostas de rede ou para tratar de erros em toda a cadeia se uma não retornar. Em seguida, ele pode se reinicializar e aguardar os mesmos 3 eventos.
colintheshots de
Mais um comentário relacionado: RX é basicamente a mônada de continuação, que se relaciona ao CPS, se não me engano, isso também pode explicar como / porque RX é bom para o problema de callback / inferno.
jhegedus
30

Apenas responda à pergunta: você poderia, por favor, mostrar também como o RX resolve o "problema do callback hell" naquele exemplo simples?

A mágica é flatMap. Podemos escrever o seguinte código em Rx para o exemplo de @ hugomg:

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

É como se você estivesse escrevendo alguns códigos FP síncronos, mas na verdade você pode torná-los assíncronos por Scheduler.

zsxwing
fonte
26

Para abordar a questão de como Rx resolve o inferno de callback :

Primeiro, vamos descrever o inferno de callback novamente.

Imagine um caso em que devemos fazer http para obter três recursos - pessoa, planeta e galáxia. Nosso objetivo é encontrar a galáxia em que a pessoa vive. Primeiro, devemos pegar a pessoa, depois o planeta, depois a galáxia. São três retornos de chamada para três operações assíncronas.

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

Cada retorno de chamada é aninhado. Cada retorno de chamada interno depende de seu pai. Isso leva ao estilo "pirâmide da desgraça" do inferno de callback . O código parece um sinal>.

Para resolver isso em RxJs, você poderia fazer algo assim:

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

Com o operador mergeMapAKA flatMap, você pode torná-lo mais sucinto:

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

Como você pode ver, o código é simplificado e contém uma única cadeia de chamadas de método. Não temos uma "pirâmide da desgraça".

Conseqüentemente, o inferno de callback é evitado.

Caso você esteja se perguntando, as promessas são outra maneira de evitar o inferno do callback, mas as promessas são ansiosas , não preguiçosas como as observáveis ​​e (falando de modo geral) você não pode cancelá-las tão facilmente.

fantasma
fonte
Não sou um desenvolvedor JS, mas essa é uma explicação fácil
Omar Beshary
15

Inferno de retorno de chamada é qualquer código em que o uso de retornos de chamada de função em código assíncrono se torna obscuro ou difícil de seguir. Geralmente, quando há mais de um nível de indireção, o código que usa callbacks pode se tornar mais difícil de seguir, de refatorar e de testar. Um cheiro de código é vários níveis de indentação devido à passagem de várias camadas de literais de função.

Isso geralmente acontece quando o comportamento tem dependências, ou seja, quando A deve acontecer antes de B deve acontecer antes de C. Então você obtém um código como este:

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

Se você tiver muitas dependências comportamentais em seu código como essa, pode se tornar problemático rapidamente. Especialmente se ramificar ...

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

Isso não vai funcionar. Como podemos fazer com que o código assíncrono seja executado em uma determinada ordem sem ter que passar todos esses callbacks?

RX é a abreviatura de 'extensões reativas'. Não usei, mas o Google sugere que é uma estrutura baseada em eventos, o que faz sentido. Os eventos são um padrão comum para fazer o código ser executado em ordem, sem criar um acoplamento frágil . Você pode fazer C ouvir o evento 'bFinished' que só acontece depois que B é chamado de ouvir 'aFinished'. Você pode então facilmente adicionar etapas extras ou estender esse tipo de comportamento e pode facilmente testar se o seu código é executado em ordem meramente transmitindo eventos em seu caso de teste.

Jimmy Breck-McKye
fonte
1

Inferno de retorno de chamada significa que você está dentro de um retorno de chamada ou dentro de outro retorno de chamada e ele vai para a enésima chamada até que suas necessidades não sejam atendidas.

Vamos entender por meio de um exemplo de chamada ajax falsa usando a API de tempo limite definido, vamos assumir que temos uma API de receita, precisamos baixar todas as receitas.

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

No exemplo acima, após 1,5 segundo quando o temporizador expira, o código de retorno de chamada será executado, em outras palavras, por meio de nossa chamada ajax falsa, todas as receitas serão baixadas do servidor. Agora precisamos baixar os dados de uma receita específica.

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

Para baixar os dados de uma receita específica, escrevemos o código em nosso primeiro retorno de chamada e passamos o Id da receita

Agora, digamos que precisamos baixar todas as receitas do mesmo editor da receita cujo 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>

Para preencher nossas necessidades, que é baixar todas as receitas do nome da editora suru, escrevemos o código dentro de nosso segundo retorno. É claro que escrevemos uma cadeia de retorno de chamada chamada inferno de retorno de chamada.

Se quiser evitar o inferno do retorno de chamada, você pode usar Promise, que é o recurso js es6, cada promessa recebe um retorno de chamada que é chamado quando uma promessa é cumprida. O retorno de chamada de promessa tem duas opções: é resolvido ou rejeitado. Suponha que sua chamada de API seja bem-sucedida, você pode chamar resolve e passar dados por meio do resolve ; você pode obter esses dados usando then () . Mas se sua API falhar, você pode usar rejeitar, usar catch para detectar o erro. Lembre-se de uma promessa, sempre use então para resolver e pegue para rejeitar

Vamos resolver o problema anterior do callback hell usando uma 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>

Agora baixe a receita particular:

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

Agora podemos escrever outra chamada de método allRecipeOfAPublisher como getRecipe, que também retornará uma promessa, e podemos escrever outro then () para receber a promessa de resolução de allRecipeOfAPublisher, espero que neste ponto você possa fazer isso sozinho.

Portanto, aprendemos como construir e consumir promessas, agora vamos tornar o consumo de uma promessa mais fácil usando async / await, que é apresentado no 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>

No exemplo acima, usamos uma função assíncrona porque ela será executada em segundo plano, dentro da função assíncrona usamos a palavra-chave await antes de cada método que retorna ou é uma promessa porque esperar nessa posição até que a promessa seja cumprida, em outras palavras no códigos abaixo até getIds concluído resolvido ou rejeitado programa irá parar de executar códigos abaixo daquela linha quando IDs retornados, então chamamos novamente a função getRecipe () com um id e esperamos usando a palavra-chave await até que os dados retornassem. Então foi assim que finalmente nos recuperamos do inferno de callback.

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

Para usar o await, precisaremos de uma função assíncrona, podemos retornar uma promessa, então use para resolver promessa e cath para rejeitar promessa

do exemplo acima:

 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);
        });
senhor
fonte
0

Uma maneira de evitar o inferno do retorno de chamada é usar o FRP, que é uma "versão aprimorada" do RX.

Comecei a usar o FRP recentemente porque encontrei uma boa implementação dele chamada Sodium( http://sodium.nz/ ).

Um código típico se parece com isto (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()é um Streamque é acionado se selectedNode(que é a Cell) muda, NodeEditorWidgetentão é atualizado de forma correspondente.

Portanto, dependendo do conteúdo do selectedNode Cell, o editado atualmente Noteserá alterado.

Este código evita Callback-s inteiramente, quase, Cacllback-s são empurrados para a "camada externa" / "superfície" do aplicativo, onde a lógica de manipulação de estado faz interface com o mundo externo. Não há retornos de chamada necessários para propagar dados dentro da lógica de tratamento de estado interno (que implementa uma máquina de estado).

O código-fonte completo está aqui

O snippet de código acima corresponde ao seguinte exemplo simples de Criar / Exibir / Atualizar:

insira a descrição da imagem aqui

Este código também envia atualizações para o servidor, portanto, as alterações nas Entidades atualizadas são salvas no servidor automaticamente.

Todo o tratamento de eventos é feito usando Streams e Cells. Esses são conceitos de FRP. Os retornos de chamada são necessários apenas quando a lógica do FRP faz interface com o mundo externo, como entrada do usuário, edição de texto, pressionamento de um botão, retorno de chamada AJAX.

O fluxo de dados é descrito explicitamente, de forma declarativa, usando FRP (implementado pela biblioteca Sodium), portanto, nenhuma manipulação de eventos / lógica de retorno de chamada é necessária para descrever o fluxo de dados.

FRP (que é uma versão mais "estrita" de RX) é uma maneira de descrever um gráfico de fluxo de dados, que pode conter nós que contêm estado. Os eventos acionam mudanças de estado no estado que contém os nós (chamados de Cells).

O sódio é uma biblioteca FRP de ordem superior, o que significa que usando a flatMap/ switchprimitiva pode-se reorganizar o gráfico de fluxo de dados em tempo de execução.

Recomendo dar uma olhada no livro Sodium , ele explica em detalhes como o FRP se livra de todos os Callbacks que não são essenciais para descrever a lógica do fluxo de dados que tem a ver com a atualização do estado dos aplicativos em resposta a alguns estímulos externos.

Usando o FRP, apenas os retornos de chamada precisam ser mantidos que descrevem a interação com o mundo externo. Em outras palavras, o fluxo de dados é descrito de maneira funcional / declarativa quando se usa uma estrutura FRP (como o Sodium) ou quando se usa uma estrutura "semelhante ao FRP" (como RX).

O sódio também está disponível para Javascript / Typescript.

Jhegedus
fonte
-3

Se você não tem um conhecimento sobre callback e hell callback, não há problema. A primeira coisa é que call back e call back hell.Por exemplo: Hell call back é como um podemos armazenar uma classe dentro de uma classe.Como você ouviu sobre isso aninhado na linguagem C, C ++. Aninhado Significa que uma classe dentro de outra classe.

Raghav Arora
fonte
A resposta será mais útil se contiver um snippet de código para mostrar o que é 'Callback hell' e o mesmo snippet de código com Rx depois de remover 'callback hell'
rafa
-4

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

simplifica assim:

    // executa tarefas sequenciais encadeadas
    jj.script ([
        // primeira tarefa
        função (próxima) {
            // no final deste processo 'próximo' aponte para a segunda tarefa e execute-a 
            callAsyncProcess1 (próximo);
        },
      // segunda tarefa
      função (próxima) {
        // no final deste processo 'próximo' aponte para a terceira tarefa e execute-a 
        callAsyncProcess2 (próximo);
      },
      // trigésima tarefa
      função (próxima) {
        // no final deste processo 'próximo' aponte para (se houver) 
        callAsyncProcess3 (próximo);
      },
    ]);

cicciodarkast
fonte
considere ultracompacto como este github.com/Javanile/Jazz.js/wiki/Script-showcase
cicciodarkast