¿Cómo mantiene legible el código con continuaciones / devoluciones de llamada?

10

Resumen: ¿Existen algunos patrones de mejores prácticas bien establecidos que pueda seguir para mantener mi código legible a pesar de usar código asincrónico y devoluciones de llamada?


Estoy usando una biblioteca de JavaScript que hace muchas cosas de forma asíncrona y depende en gran medida de las devoluciones de llamada. Parece que escribir un método simple de "carga A, carga B, ..." se vuelve bastante complicado y difícil de seguir usando este patrón.

Permítanme dar un ejemplo (artificial). Digamos que quiero cargar un montón de imágenes (asincrónicamente) desde un servidor web remoto. En C # / async, escribiría algo como esto:

disableStartButton();

foreach (myData in myRepository) {
    var result = await LoadImageAsync("http://my/server/GetImage?" + myData.Id);
    if (result.Success) {
        myData.Image = result.Data;
    } else {
        write("error loading Image " + myData.Id);
        return;
    }
}

write("success");
enableStartButton();

El diseño del código sigue el "flujo de eventos": primero, se deshabilita el botón de inicio, luego se cargan las imágenes ( awaitasegura que la IU se mantenga receptiva) y luego se vuelve a habilitar el botón de inicio.

En JavaScript, usando devoluciones de llamada, se me ocurrió esto:

disableStartButton();

var count = myRepository.length;

function loadImage(i) {
    if (i >= count) {
        write("success");
        enableStartButton();
        return;
    }

    myData = myRepository[i];
    LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) { 
            if (success) {
                myData.Image = data;
            } else {
                write("error loading image " + myData.Id);
                return;
            }
            loadImage(i+1); 
        }
    );
}

loadImage(0);

Creo que los inconvenientes son obvios: tuve que volver a trabajar el bucle en una llamada recursiva, el código que se supone que debe ejecutarse al final está en algún lugar en el medio de la función, el código que comienza la descarga ( loadImage(0)) está en la parte inferior, y generalmente es mucho más difícil de leer y seguir. Es feo y no me gusta.

Estoy seguro de que no soy el primero en encontrar este problema, así que mi pregunta es: ¿Existen algunos patrones de mejores prácticas bien establecidos que pueda seguir para mantener mi código legible a pesar de usar código asincrónico y devoluciones de llamada?

Heinzi
fuente
¿Hay alguna razón en particular para que sus llamadas "asíncronas" se realicen secuencialmente? ¿Es esta una versión simplificada de algún otro código?
Izkata
@Izkata: La razón es que quería ser amable con el servidor remoto (= no bombardearlo con cientos de solicitudes simultáneas). No es un requisito establecido en piedra. Sí, es una versión simplificada del código, LoadImageAsyncde hecho es una llamada Ext.Ajax.requestde Sencha Touch.
Heinzi
1
La mayoría de los navegadores no le permitirán martillar el servidor de todos modos, solo ponen en cola las solicitudes y comienzan la siguiente cuando se completa una de las anteriores.
Izkata
¡Dios! muchos malos consejos aquí. Ninguna cantidad de patrones de diseño te ayudará. Mire en async.js , async.waterfalles su respuesta.
Salman von Abbas

Respuestas:

4

Es muy poco probable que pueda lograr con js simple el mismo nivel de concisión y expresividad al trabajar con devoluciones de llamada que tiene C # 5. El compilador hace el trabajo al escribir toda esa plantilla para usted, y hasta que js runtimes lo haga, aún tendrá que pasar una devolución de llamada ocasional aquí y allá.

Sin embargo, es posible que no siempre desee reducir las devoluciones de llamada al nivel de simplicidad del código lineal: lanzar funciones no tiene que ser feo, hay un mundo entero trabajando con este tipo de código, y se mantienen cuerdas sin asyncy await.

Por ejemplo, use funciones de orden superior (mi js puede estar un poco oxidado):

// generic - this is a library function
function iterateAsync(iterator, action, onSuccess, onFailure) {
var item = iterator();
if(item == null) { // exit condition
    onSuccess();
    return;
}
action(item,
    function (success) {
        if(success)
            iterateAsync(iterator, action, onSuccess, onFailure);
        else
            onFailure();
    });
}


// calling code
var currentImage = 0;
var imageCount = 42;

// you know your library function expects an iterator with no params, 
// and an async action with the current item and its continuation as params
iterateAsync(
// this is your iterator
function () {   
    if(currentImage >= imageCount)
        return null;
    return "http://my/server/GetImage?" + (currentImage++);
},

// this is your action - coincidentally, no adaptor for the correct signature is necessary
LoadImageAsync,

// these are your outs
function () { console.log("All OK."); },
function () { console.log("FAILED!"); }
);
vski
fuente
2

Me tomó un poco descifrar por qué lo haces de esta manera, pero creo que esto podría estar cerca de lo que quieres.

function loadImages() {
   var countRemainingToLoad = 0;
   var failures = 0;

   myRepository.each(function (myData) {
      countRemainingToLoad++;

      LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) {
            if (success) {
                myData.Image = data;
            } else {
                write("error loading image " + myData.Id);
                failures++;
            }
            countRemainingToLoad--;
            if (countRemainingToLoad == 0 && failures == 0) {
                enableStartButton();
            }
        }
    );
}

disableStartButton();
loadImages();

Primero apaga tantas solicitudes AJAX como puede hacer simultáneamente, y espera hasta que se completen todas antes de habilitar el botón Inicio. Esto será más rápido que una espera secuencial y, creo, es mucho más fácil de seguir.

EDITAR : tenga en cuenta que esto supone que tiene .each()disponible, y que myRepositoryes una matriz. Tenga cuidado con la iteración de bucle que usa aquí en lugar de eso, si no está disponible, esta aprovecha las propiedades de cierre para la devolución de llamada. Sin embargo, no estoy seguro de lo que tiene disponible, ya que LoadImageAsyncparece ser parte de una biblioteca especializada: no veo resultados en Google.

Izkata
fuente
+1, tengo .each()disponible, y, ahora que lo mencionas, no es estrictamente necesario hacer la carga secuencialmente. Definitivamente voy a probar su solución. (Aunque aceptaré la respuesta de vski, ya que está más cerca de la pregunta original y más general).
Heinzi
@Heinzi Estuvo de acuerdo en lo diferente que es, pero (creo) que este también es un ejemplo decente de cómo los diferentes idiomas tienen diferentes formas de manejar lo mismo. Si algo se siente incómodo al traducirlo a un idioma diferente, probablemente haya una manera más fácil de hacerlo usando un paradigma diferente.
Izkata
1

Descargo de responsabilidad: esta respuesta no responde específicamente a su problema, es una respuesta genérica a la pregunta: "¿Existen algunos patrones de mejores prácticas bien establecidos que pueda seguir para mantener mi código legible a pesar de usar código asincrónico y devoluciones de llamada?"

Por lo que sé, no hay un patrón "bien establecido" para manejar esto. Sin embargo, he visto dos tipos de métodos utilizados para evitar las pesadillas de devoluciones de llamada anidadas.

1 / Uso de funciones con nombre en lugar de devoluciones de llamadas anónimas

    function start() {
        mongo.findById( id, handleDatas );
    }

    function handleDatas( datas ) {
        // Handle the datas returned.
    }

De esta manera, evita el anidamiento enviando la lógica de la función anónima en otra función.

2 / Uso de una biblioteca de gestión de flujo. Me gusta usar Step , pero es solo una cuestión de preferencia. Es el que usa LinkedIn, por cierto.

    Step( {
        function start() {
            // the "this" magically sends to the next function.
            mongo.findById( this );
        },

        function handleDatas( el ) {
            // Handle the datas.
            // Another way to use it is by returning a value,
            // the value will be sent to the next function.
            // However, this is specific to Step, so look at
            // the documentation of the library you choose.
            return value;
        },

        function nextFunction( value ) {
            // Use the returned value from the preceding function
        }
    } );

Uso una biblioteca de administración de flujo cuando uso muchas devoluciones de llamada anidadas, porque es mucho más legible cuando hay mucho código usándola.

Florian Margaine
fuente
0

En pocas palabras, JavaScript no tiene el azúcar sintáctico de await.
Pero mover la parte "final" al final de la función es fácil; y con una función anónima que se ejecuta inmediatamente, podemos evitar declararle una referencia.

disableStartButton();

(function(i, count) {
    var loadImage = arguments.callee;
    myData = myRepository[i];

    LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) { 
            if (!success) {
                write("error loading image " + myData.Id);

            } else {
                myData.Image = data;
                if (i < count) {
                    loadImage(i + 1, count);

                } else {
                    write("success");
                    enableStartButton();
                    return;

                }

            }

        }
    );
})(0, myRepository.length);

También puede pasar la parte "final" como una devolución de llamada exitosa a la función.

Rey gitano
fuente