javascript - observables - rxjs package
¿Qué es el "infierno de devolución de llamada" y cómo y por qué RX lo resuelve? (5)
¿Puede alguien 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 configuraciones) ocurre el "problema del retrollamada"?
¿Por qué ocurre?
¿El "infierno de devolución de llamada" siempre está relacionado con cálculos asincrónicos?
¿O puede ocurrir un "infierno de devolución de llamada" también en una sola aplicación con subprocesos?
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 recibí una respuesta clara.
Después de explicar el "infierno de devolución de llamada" en un ejemplo simple, ¿podría mostrar también cómo RX resuelve el "problema del retrollamada" en ese simple ejemplo?
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 la anidación larga de funciones asíncronas en Node.js
El problema en Javascript es que la única forma de "congelar" un cómputo y ejecutar el "resto de él" último (de forma asíncrona) es poner "el resto" dentro de una devolución de llamada.
Por ejemplo, supongamos que quiero ejecutar código que se ve así:
x = getData();
y = getMoreData(x);
z = getMoreData(y);
...
¿Qué sucede si ahora quiero hacer que las funciones getData sean asincrónicas, lo que significa que tengo la oportunidad de ejecutar algún otro código mientras espero que devuelvan sus valores? En Javascript, la única manera sería recodificar todo lo que toca un cálculo asincrónico usando el estilo de continuación de paso :
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 configuraciones) ocurre el "problema del retrollamada"?
¡Cuando tienes muchas funciones de devolución de llamada en tu código! Cada vez es más difícil trabajar con ellos, ya que hay más de ellos en el código y se vuelve especialmente malo cuando necesitas hacer bucles, bloques de prueba y cosas por el estilo.
Por ejemplo, hasta donde yo sé, en JavaScript la única forma de ejecutar una serie de funciones asíncronas donde una se ejecuta después de las devoluciones anteriores es usar una función recursiva. No puedes usar un bucle for.
//we would like to write
for(var i=0; i<10; i++){
doSomething(i);
}
blah();
En cambio, podríamos necesitar terminar escribiendo:
function loop(i, onDone){
if(i >= 10){
onDone()
}else{
doSomething(i, function(){
loop(i+1, onDone);
});
}
}
loop(0, function(){
blah();
});
//ugh!
El número de preguntas que la etiqueta de JavaScript obtiene preguntando cómo hacer esto debe ser un testimonio de lo confuso que es :)
3) ¿Por qué ocurre?
Se produce porque en JavaScript la única forma de retrasar un cálculo para que se ejecute después de que la llamada asíncrona regrese 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 síncrono tradicional para que termine con devoluciones de llamada anidadas en todas partes.
4) ¿O puede ocurrir un "infierno de devolución de llamada" también en una sola aplicación con subprocesos?
La programación asíncrona tiene que ver con la simultaneidad, mientras que la de un solo hilo tiene que ver con el paralelismo. Los dos conceptos en realidad no son lo mismo.
Todavía puede tener código concurrente en un único contexto de subprocesos. De hecho, JavaScript, la reina del infierno de devolución de llamada, tiene un solo hilo.
Concurrencia vs Paralelismo: ¿Cuál es la diferencia?
5) ¿podría mostrar también cómo RX resuelve el "problema del retrollamada" en ese simple ejemplo?
No sé nada sobre RX en particular, pero generalmente este problema se resuelve agregando soporte nativo para el cálculo asincrónico en el lenguaje de programación. Esto puede ser muy complicado y puede tener diferentes nombres (asincrónico, generadores, corutinas, callcc, ...). Para un ejemplo concreto de mierda, en Python podemos usar generadores para codificar ese ejemplo de bucle 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" haga una pausa en nuestro bucle 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 "al revés" como teníamos que hacer en esa función de loop
recursivo.
Para abordar la cuestión 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 debemos hacer http para obtener tres recursos: persona, planeta y galaxia. Nuestro objetivo es encontrar la galaxia en la que vive la persona. Primero debemos obtener a la persona, luego al planeta, luego a la galaxia. Eso es 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 lleva al estilo de "pirámide del destino" del infierno de devolución de llamada . El código se ve como un signo>.
Para resolver esto en RxJs podrías hacer algo como esto:
getPerson(person)
.map(person => getPlanet(person))
.map(planet => getGalaxy(planet))
.mergeAll()
.subscribe(galaxy => console.log(galaxy));
Con el operador mergeMap
AKA flatMap
puede hacerlo más breve:
getPerson(person)
.mergeMap(person => getPlanet(person))
.mergeMap(planet => getGalaxy(planet))
.subscribe(galaxy => console.log(galaxy));
Como puede ver, el código se aplana y contiene una sola cadena de llamadas a métodos. No tenemos una "pirámide de fatalidad".
Por lo tanto, se evita el infierno de devolución de llamada.
Retrollamada es cualquier código en el que el uso de devoluciones de funciones en el código asíncrono se vuelve oscuro o difícil de seguir. En general, cuando hay más de un nivel de direccionamiento indirecto, el código que utiliza devoluciones de llamada puede ser más difícil de seguir, más difícil de refactorizar y más difícil de probar. Un olor de código es múltiples niveles de sangría debido al paso de múltiples capas de literales de funciones.
Esto sucede a menudo cuando el comportamiento tiene dependencias, es decir, cuando A debe pasar antes de que B pase antes que 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 ser 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 asíncrono 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 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 B se llame para escuchar ''aFinished''. A continuación, puede agregar fácilmente pasos adicionales o extender este tipo de comportamiento, y puede probar fácilmente que su código se ejecuta en orden simplemente mediante la transmisión de eventos en su caso de prueba.
Simplemente responda la pregunta: ¿podría mostrar también cómo RX resuelve el "problema del retrollamada" en ese simple ejemplo?
La magia es flatMap
. Podemos escribir el siguiente código en el ejemplo de Rx for @ hugomg:
def getData() = Observable[X]
getData().flatMap(x -> Observable[Y])
.flatMap(y -> Observable[Z])
.map(z -> ...)...
Es como si estuvieras escribiendo algunos códigos FP sincrónicos, pero en realidad puedes hacerlos asincrónicos por Scheduler
.
Utilice jazz.js https://github.com/Javanile/Jazz.js
se simplifica así:
// run sequential task chained jj.script([ // first task function(next) { // at end of this process ''next'' point to second task and run it callAsyncProcess1(next); }, // second task function(next) { // at end of this process ''next'' point to thirt task and run it callAsyncProcess2(next); }, // thirt task function(next) { // at end of this process ''next'' point to (if have) callAsyncProcess3(next); }, ]);