¿Qué es el "infierno de devolución de llamada" y cómo y por qué lo resuelve RX?


113

¿Alguien puede dar una definición clara junto con un ejemplo simple que explique qué es un "infierno de devolución de llamada" para alguien que no conoce JavaScript y node.js?

¿Cuándo (en qué tipo de configuración) ocurre el "problema del infierno de devolución de llamada"?

¿Por qué ocurre?

¿El "infierno de devolución de llamada" está siempre relacionado con los cálculos asincrónicos?

¿O puede ocurrir el "infierno de devolución de llamada" también en una aplicación de un solo subproceso?

Tomé el curso reactivo en Coursera y Erik Meijer dijo en una de sus conferencias que RX resuelve el problema del "infierno de devolución de llamada". Pregunté qué es un "infierno de devolución de llamada" en el foro de Coursera, pero no obtuve una respuesta clara.

Después de explicar el "infierno de la devolución de llamada" en un ejemplo simple, ¿podría también mostrar cómo RX resuelve el "problema del infierno de la devolución de llamada" en ese ejemplo simple?

Respuestas:


136

1) ¿Qué es un "infierno de devolución de llamada" para alguien que no conoce javascript y node.js?

Esta otra pregunta tiene algunos ejemplos del infierno de devolución de llamada de Javascript: cómo evitar el anidamiento prolongado de funciones asincrónicas en Node.js

El problema en Javascript es que la única forma de "congelar" un cálculo y hacer que el "resto" se ejecute más tarde (asincrónicamente) es poner "el resto" dentro de una devolución de llamada.

Por ejemplo, digamos que quiero ejecutar un código que se ve así:

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

¿Qué sucede si ahora quiero que las funciones getData sean asincrónicas, lo que significa que tengo la oportunidad de ejecutar otro código mientras espero que devuelvan sus valores? En Javascript, la única forma sería reescribir todo lo que toca un cálculo asíncrono usando el estilo de paso de continuación :

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

No creo que deba convencer a nadie de que esta versión es más fea que la anterior. :-)

2) ¿Cuándo (en qué tipo de configuración) ocurre el "problema del infierno de devolución de llamada"?

¡Cuando tienes muchas funciones de devolución de llamada en tu código! Se vuelve más difícil trabajar con ellos cuanto más tenga en su código y se vuelve particularmente malo cuando necesita hacer bucles, bloques try-catch y cosas así.

Por ejemplo, hasta donde yo sé, en JavaScript la única forma de ejecutar una serie de funciones asincrónicas donde una se ejecuta después de los retornos anteriores es usando una función recursiva. No puedes usar un bucle for.

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

En cambio, es posible que tengamos que terminar escribiendo:

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

//ugh!

La cantidad de preguntas que recibimos aquí en StackOverflow sobre cómo hacer este tipo de cosas es un testimonio de lo confuso que es :)

3) ¿Por qué ocurre?

Ocurre porque en JavaScript la única forma de retrasar un cálculo para que se ejecute después de que regrese la llamada asincrónica es poner el código retrasado dentro de una función de devolución de llamada. No puede retrasar el código que se escribió en el estilo sincrónico tradicional, por lo que termina con devoluciones de llamada anidadas en todas partes.

4) ¿O puede ocurrir un "infierno de devolución de llamada" también en una aplicación de un solo hilo?

La programación asincrónica tiene que ver con la concurrencia, mientras que un solo hilo tiene que ver con el paralelismo. En realidad, los dos conceptos no son lo mismo.

Aún puede tener código concurrente en un contexto de un solo hilo. De hecho, JavaScript, la reina del infierno de las devoluciones de llamada, es de un solo hilo.

¿Cuál es la diferencia entre concurrencia y paralelismo?

5) ¿Podría mostrar también cómo RX resuelve el "problema del infierno de devolución de llamada" en ese simple ejemplo?

No sé nada sobre RX en particular, pero generalmente este problema se resuelve agregando soporte nativo para computación asincrónica en el lenguaje de programación. Las implementaciones pueden variar e incluir: async, generators, coroutines y callcc.

En Python podemos implementar ese ejemplo de bucle anterior con algo como:

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

myGen = myLoop()

Este no es el código completo, pero la idea es que el "rendimiento" pausa nuestro ciclo for hasta que alguien llame a myGen.next (). Lo importante es que aún podríamos escribir el código usando un bucle for, sin necesidad de convertir la lógica "de adentro hacia afuera" como teníamos que hacer en esa loopfunción recursiva .


Entonces, ¿el infierno de devolución de llamada solo puede ocurrir en una configuración asíncrona? Si mi código es completamente sincrónico (es decir, sin simultaneidad), entonces el "infierno de devolución de llamada" no puede ocurrir si entiendo tu respuesta correctamente, ¿es así?
jhegedus

El infierno de devolución de llamada tiene más que ver con lo molesto que es codificar usando el estilo de paso de continuación. En teoría, aún podría reescribir todas sus funciones usando el estilo CPS incluso para un programa normal (el artículo de wikipedia tiene algunos ejemplos) pero, por una buena razón, la mayoría de la gente no hace eso. Por lo general, solo usamos el estilo de paso de continuación si nos vemos obligados a hacerlo, que es el caso de la programación asíncrona de JavaScript.
hugomg

Por cierto, busqué en Google las extensiones reactivas y tengo la impresión de que son más similares a una biblioteca de Promise y no a una extensión de lenguaje que introduce sintaxis asíncrona. Las promesas ayudan a lidiar con el anidamiento de devolución de llamada y con el manejo de excepciones, pero no son tan ordenadas como las extensiones de sintaxis. El ciclo for sigue siendo molesto para el código y aún necesita traducir el código del estilo síncrono al estilo de promesa.
hugomg

1
Debo aclarar cómo RX generalmente hace un mejor trabajo. RX es declarativo. Puede declarar cómo responderá el programa a los eventos cuando ocurran más tarde sin afectar ninguna otra lógica del programa. Esto le permite separar el código del bucle principal del código de manejo de eventos. Puede manejar fácilmente detalles como el orden de eventos asíncronos que son una pesadilla cuando se usan variables de estado. Descubrí que RX era la implementación más limpia para realizar una nueva solicitud de red después de que se devolvieran 3 respuestas de red o para manejar el error de toda la cadena si una no regresa. Luego, puede reiniciarse y esperar los mismos 3 eventos.
colintheshots

Un comentario más relacionado: RX es básicamente la mónada de continuación, que se relaciona con CPS si no me equivoco, esto también podría explicar cómo / por qué RX es bueno para el problema de devolución de llamada / infierno.
jhegedus

30

Simplemente responda la pregunta: ¿podría mostrar también cómo RX resuelve el "problema del infierno de devolución de llamada" en ese simple ejemplo?

La magia es flatMap. Podemos escribir el siguiente código en Rx para el ejemplo de @ hugomg:

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

Es como si estuviera escribiendo algunos códigos FP sincrónicos, pero en realidad puede hacerlos asincrónicos mediante Scheduler.


26

Para abordar la pregunta de cómo Rx resuelve el infierno de devolución de llamada :

Primero describamos el infierno de devolución de llamada nuevamente.

Imagine un caso en el que debamos hacer http para obtener tres recursos: persona, planeta y galaxia. Nuestro objetivo es encontrar la galaxia en la que vive la persona. Primero debemos encontrar a la persona, luego el planeta, luego la galaxia. Son tres devoluciones de llamada para tres operaciones asincrónicas.

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

Cada devolución de llamada está anidada. Cada devolución de llamada interna depende de su padre. Esto conduce al estilo de la "pirámide de la fatalidad" del infierno de devolución de llamada . El código parece un signo>.

Para resolver esto en RxJs, podría hacer algo como esto:

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

Con el operador mergeMapAKA flatMappodría hacerlo más conciso:

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

Como puede ver, el código es plano y contiene una única cadena de llamadas a métodos. No tenemos una "pirámide de la perdición".

Por lo tanto, se evita el infierno de devolución de llamada.

En caso de que se lo esté preguntando, las promesas son otra forma de evitar el infierno de las devoluciones de llamada, pero las promesas son impacientes , no perezosas como los observables y (en términos generales) no puede cancelarlas tan fácilmente.


No soy un desarrollador de JS, pero esta es una explicación fácil
Omar Beshary

15

El infierno de devolución de llamada es cualquier código donde el uso de devoluciones de llamada de función en código asincrónico se vuelve oscuro o difícil de seguir. Generalmente, cuando hay más de un nivel de direccionamiento indirecto, el código que usa devoluciones de llamada puede volverse más difícil de seguir, más difícil de refactorizar y más difícil de probar. Un olor a código son múltiples niveles de sangría debido al paso de múltiples capas de literales de función.

Esto sucede a menudo cuando el comportamiento tiene dependencias, es decir, cuando A debe suceder antes de que B debe suceder antes de C. Entonces obtienes un código como este:

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

Si tiene muchas dependencias de comportamiento en su código como este, puede volverse problemático rápidamente. Especialmente si se 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 {
          ...
        }
    }
});

Esto no servirá. ¿Cómo podemos hacer que el código asincrónico se ejecute en un orden determinado sin tener que pasar todas estas devoluciones de llamada?

RX es la abreviatura de 'extensiones reactivas'. No lo he usado, pero buscar en Google sugiere que es un marco basado en eventos, lo cual tiene sentido. Los eventos son un patrón común para hacer que el código se ejecute en orden sin crear un acoplamiento frágil . Puede hacer que C escuche el evento 'bFinished', que solo ocurre después de que se llame a B escuchando 'aFinished'. Luego, puede agregar pasos adicionales o extender este tipo de comportamiento fácilmente , y puede probar fácilmente que su código se ejecuta en orden simplemente transmitiendo eventos en su caso de prueba.


1

Devolver llamada al infierno significa que está dentro de una devolución de llamada o dentro de otra devolución de llamada y va a la enésima llamada hasta que no se satisfagan sus necesidades.

Entendamos a través de un ejemplo de llamada falsa ajax usando la API de tiempo de espera establecido, supongamos que tenemos una API de recetas, necesitamos descargar todas las recetas.

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

En el ejemplo anterior, después de 1,5 segundos, cuando el temporizador expira, el código de devolución de llamada se ejecutará, en otras palabras, a través de nuestra llamada falsa ajax, todas las recetas se descargarán del servidor. Ahora necesitamos descargar los datos de una receta en particular.

<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 descargar los datos de una receta en particular, escribimos código dentro de nuestra primera devolución de llamada y pasamos el Id de la receta.

Ahora digamos que necesitamos descargar todas las recetas del mismo editor de la receta cuyo id es 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 satisfacer nuestras necesidades, que es descargar todas las recetas del nombre del editor suru, escribimos el código dentro de nuestra segunda devolución de llamada. Está claro que escribimos una cadena de devolución de llamada que se llama infierno de devolución de llamada.

Si desea evitar el infierno de devolución de llamada, puede usar Promise, que es la función js es6, cada promesa recibe una devolución de llamada que se llama cuando una promesa está completa. promesa de devolución de llamada tiene dos opciones: se resuelve o se rechaza. Suponga que su llamada a la API es exitosa, puede llamar a resolver y pasar datos a través de la resolución , puede obtener estos datos usando then () . Pero si su API falló, puede usar el rechazo, use catch para detectar el error. Recuerde que una promesa siempre use entonces para resolver y atrapar para rechazar

Resolvamos el problema del infierno de devolución de llamada anterior usando una promesa.

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

Ahora descargue una receta 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>

Ahora podemos escribir otro método llamado allRecipeOfAPublisher como getRecipe que también devolverá una promesa, y podemos escribir otro then () para recibir la promesa de resolución para allRecipeOfAPublisher, espero que en este punto puedas hacerlo tú mismo.

Así que aprendimos cómo construir y consumir promesas, ahora hagamos que consumir una promesa sea más fácil usando async / await que se introduce en 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>

En el ejemplo anterior, usamos una función asíncrona porque se ejecutará en segundo plano, dentro de la función asíncrona usamos la palabra clave await antes de cada método que devuelve o es una promesa porque esperar en esa posición hasta que se cumpla esa promesa, en otras palabras en el debajo de los códigos hasta que getIds se complete resuelto o el programa de rechazo dejará de ejecutar códigos debajo de esa línea cuando los ID regresen, luego llamamos nuevamente a la función getRecipe () con un id y esperamos usando la palabra clave await hasta que se devuelvan los datos. Así es como finalmente nos recuperamos del infierno de las devoluciones de llamada.

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

Para usar await, necesitaremos una función asíncrona, podemos devolver una promesa, así que use luego para resolver promesa y cath para rechazar promesa

del ejemplo anterior:

 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

Una forma en que se puede evitar el infierno de la devolución de llamada es usar FRP, que es una "versión mejorada" de RX.

Comencé a usar FRP recientemente porque encontré una buena implementación llamada Sodium( http://sodium.nz/ ).

Un código típico se ve así (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()es a Streamque se activa si selectedNode(que es a Cell) cambia, NodeEditorWidgetentonces se actualiza en consecuencia.

Entonces, dependiendo del contenido del selectedNode Cell, el editado actualmente Notecambiará.

Este código evita los Callback-s por completo, casi, los Cacllback-s son empujados a la "capa externa" / "superficie" de la aplicación, donde la lógica de manejo del estado interactúa con el mundo externo. No se necesitan devoluciones de llamada para propagar datos dentro de la lógica de manejo de estado interno (que implementa una máquina de estado).

El código fuente completo está aquí

El fragmento de código anterior corresponde al siguiente ejemplo simple de Crear / Mostrar / Actualizar:

ingrese la descripción de la imagen aquí

Este código también envía actualizaciones al servidor, por lo que los cambios en las Entidades actualizadas se guardan en el servidor automáticamente.

Todo el manejo de eventos se realiza mediante Streams y Cells. Estos son conceptos de FRP. Las devoluciones de llamada solo son necesarias cuando la lógica de FRP interactúa con el mundo externo, como la entrada del usuario, la edición de texto, la presión de un botón, las devoluciones de llamadas AJAX.

El flujo de datos se describe explícitamente, de manera declarativa utilizando FRP (implementado por la biblioteca Sodium), por lo que no se necesita ninguna lógica de manejo de eventos / devolución de llamada para describir el flujo de datos.

FRP (que es una versión más "estricta" de RX) es una forma de describir un gráfico de flujo de datos, que puede contener nodos que contienen estado. Los eventos desencadenan cambios de estado en el estado que contiene los nodos (llamados Cells).

El sodio es una biblioteca de FRP de orden superior, lo que significa que el uso de la primitiva flatMap/ switchpuede reorganizar el gráfico de flujo de datos en tiempo de ejecución.

Recomiendo echar un vistazo al libro Sodium , que explica en detalle cómo FRP elimina todas las devoluciones de llamada que no son esenciales para describir la lógica del flujo de datos que tiene que ver con la actualización del estado de las aplicaciones en respuesta a algunos estímulos externos.

Al usar FRP, solo se deben mantener aquellas devoluciones de llamada que describen la interacción con el mundo externo. En otras palabras, el flujo de datos se describe de una manera funcional / declarativa cuando se usa un marco FRP (como Sodium), o cuando se usa un marco "similar a FRP" (como RX).

El sodio también está disponible para Javascript / Typecript.


-3

Si no tienes conocimiento sobre la devolución de llamada y la devolución de llamada del infierno, no hay problema. Lo primero es devolver la llamada y devolver la llamada al infierno. Por ejemplo: la devolución de llamada del infierno es como si pudiéramos almacenar una clase dentro de una clase. sobre eso anidado en C, lenguaje C ++. Anidado Significa que una clase dentro de otra clase.


La respuesta será más útil si contiene un fragmento de código para mostrar qué es el 'infierno de devolución de llamada' y el mismo fragmento de código con Rx después de eliminar el 'infierno de devolución de llamada'
rafa

-4

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

se simplifica así:

    // ejecutar tarea secuencial encadenada
    jj.script ([
        // primera tarea
        función (siguiente) {
            // al final de este proceso 'siguiente' apunte a la segunda tarea y ejecútela 
            callAsyncProcess1 (siguiente);
        },
      // segunda tarea
      función (siguiente) {
        // al final de este proceso 'siguiente' apunte a la treinta tarea y ejecútela 
        callAsyncProcess2 (siguiente);
      },
      // Treinta tarea
      función (siguiente) {
        // al final de este proceso 'siguiente' apunte a (si tiene) 
        callAsyncProcess3 (siguiente);
      },
    ]);


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.