Come mantieni il codice con continuazioni / callback leggibili?

10

Riepilogo: Esistono alcuni modelli best practice consolidati che posso seguire per mantenere leggibile il mio codice nonostante l'utilizzo di codice asincrono e callback?

Sto usando una libreria JavaScript che fa un sacco di cose in modo asincrono e si affida pesantemente ai callback. Sembra che scrivere un semplice metodo "carica A, carica B, ..." diventi piuttosto complicato e difficile da seguire usando questo modello.

Lasciatemi dare un esempio (forzato). Diciamo che voglio caricare un gruppo di immagini (in modo asincrono) da un server web remoto. In C # / async, scriverei qualcosa del genere:

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();

Il layout del codice segue il "flusso di eventi": in primo luogo, il pulsante di avvio è disabilitato, quindi le immagini vengono caricate ( await garantisce che l'interfaccia utente rimanga reattiva) e quindi il pulsante di avvio viene nuovamente abilitato.

In JavaScript, utilizzando i callback, ho trovato questo:

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

Penso che gli svantaggi siano ovvi: ho dovuto rielaborare il ciclo in una chiamata ricorsiva, il codice che dovrebbe essere eseguito alla fine è da qualche parte nel mezzo della funzione, il codice che inizia il download ( loadImage(0) ) è in fondo, ed è generalmente molto più difficile da leggere e seguire. È brutto e non mi piace.

Sono sicuro di non essere il primo a incontrare questo problema, quindi la mia domanda è: Ci sono alcuni modelli di best practice consolidati che posso seguire per mantenere il mio codice leggibile nonostante utilizzando codice asincrono e callback?

    
posta Heinzi 05.06.2012 - 11:00
fonte

4 risposte

4

È altamente improbabile che tu possa ottenere con lo stesso livello di concisione ed espressività il livello di chiarezza nel lavorare con i callback che ha C # 5. Il compilatore fa il lavoro per iscritto per te e per tutto il resto, fino a quando non lo farai, dovrai comunque passare una chiamata occasionale qua e là.

Tuttavia, potresti non voler sempre portare i callback al livello di semplicità del codice lineare - il lancio di funzioni in giro non deve essere brutto, c'è un whole world funziona con questo tipo di codice, e mantengono sano senza async e await .

Ad esempio, usa le funzioni di ordine superiore (my js potrebbe essere un po 'arrugginito):

// 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!"); }
);
    
risposta data 05.06.2012 - 12:41
fonte
2

Ci sono voluti un po 'per decodificare perché lo stai facendo in questo modo, ma penso che potrebbe essere vicino a quello che vuoi?

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();

Disattiva prima tutte le richieste AJAX che può fare contemporaneamente e attende che tutte le richieste vengano completate prima di attivare il pulsante Avvia. Questo sarà più veloce di un'attesa sequenziale e, penso, è molto più facile da seguire.

EDIT : si noti che questo presuppone che sia disponibile .each() e che myRepository sia un array. Fai attenzione a quale ciclo iterazione usi qui al posto di quello, se non è disponibile - questo sfrutta le proprietà di chiusura per il callback. Non sono sicuro di ciò che hai a disposizione, tuttavia, dato che LoadImageAsync sembra essere parte di una libreria specializzata, non vedo risultati in Google.

    
risposta data 05.06.2012 - 23:27
fonte
1

Disclaimer: questa risposta non risponde specificamente al tuo problema, è una risposta generica alla domanda: "Esistono alcuni modelli di best practice consolidati che posso seguire per mantenere il mio codice leggibile nonostante l'uso codice asincrono e callback? "

Da quello che so, non esiste un modello "ben consolidato" per gestire questo problema. Tuttavia, ho visto due tipi di metodi usati per evitare gli incubi di callback nidificati.

1 / Utilizzo di funzioni con nome invece di callback anonimi

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

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

In tal modo, si evita l'annidamento inviando la logica della funzione anonima in un'altra funzione.

2 / Utilizzo di una libreria di gestione del flusso. Mi piace usare Step , ma è solo una questione di preferenza. A proposito, è quello che usa LinkedIn.

    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 libreria di gestione del flusso quando uso molte callback annidate, perché è molto più leggibile quando c'è molto codice che la usa.

    
risposta data 06.06.2012 - 08:45
fonte
0

In parole semplici, JavaScript non ha lo zucchero sintattico di await .
Ma spostare la parte "fine" nella parte inferiore della funzione è semplice; e con una funzione anonima che si esegue in modo immediato, possiamo evitare di dichiarare un riferimento ad esso.

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

Si potrebbe anche passare la parte "fine" come callback di successo alla funzione.

    
risposta data 05.06.2012 - 12:49
fonte

Leggi altre domande sui tag