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 loop
función recursiva .
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:Es como si estuviera escribiendo algunos códigos FP sincrónicos, pero en realidad puede hacerlos asincrónicos mediante
Scheduler
.fuente
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.
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:
Con el operador
mergeMap
AKAflatMap
podría hacerlo más conciso: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.
fuente
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:
Si tiene muchas dependencias de comportamiento en su código como este, puede volverse problemático rápidamente. Especialmente si se ramifica ...
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.
fuente
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.
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.
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.
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.
Ahora descargue una receta particular:
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.
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.
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:
fuente
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):
selectedNote.updates()
es aStream
que se activa siselectedNode
(que es aCell
) cambia,NodeEditorWidget
entonces se actualiza en consecuencia.Entonces, dependiendo del contenido del
selectedNode
Cell
, el editado actualmenteNote
cambiará.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:
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
Stream
s yCell
s. 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
Cell
s).El sodio es una biblioteca de FRP de orden superior, lo que significa que el uso de la primitiva
flatMap
/switch
puede 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.
fuente
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.
fuente
Utilice jazz.js https://github.com/Javanile/Jazz.js
se simplifica así:
fuente