Come si mantiene il codice con continuazioni / callback leggibili?


10

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


Sto usando una libreria JavaScript che fa molte cose in modo asincrono e fa molto affidamento sui callback. Sembra che scrivere un semplice metodo "carica A, carica B, ..." diventa piuttosto complicato e difficile da seguire usando questo modello.

Lasciatemi fare un esempio (inventato). Diciamo che voglio caricare un sacco 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 ( awaitassicura che l'interfaccia utente rimanga reattiva) e quindi il pulsante di avvio viene nuovamente abilitato.

In JavaScript, usando i callback, ho pensato a 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 riscontrare questo problema, quindi la mia domanda è: ci sono alcuni schemi di best practice ben consolidati che posso seguire per mantenere il mio codice leggibile nonostante l'uso di codice asincrono e callback?


C'è un motivo particolare per cui le chiamate "asincrone" devono essere eseguite in sequenza? È una versione semplificata di qualche altro codice?
Izkata,

@Izkata: Il motivo è che volevo essere gentile con il server remoto (= non bombardarlo con centinaia di richieste simultanee). Non è un requisito stabilito nella pietra. Sì, è una versione semplificata del codice, LoadImageAsyncin realtà è una chiamata a Ext.Ajax.requestdi Sencha Touch.
Heinzi,

1
La maggior parte dei browser non ti consentirà comunque di martellare il server: accodano semplicemente le richieste e avviano il successivo quando uno dei precedenti viene completato.
Izkata,


Dio! molti cattivi consigli qui. Nessuna quantità di modelli di design ti aiuterà. Guarda async.js , async.waterfallè la tua risposta.
Salman von Abbas,

Risposte:


4

È altamente improbabile che tu possa ottenere con semplicità lo stesso livello di concisione ed espressività nel lavorare con callback che ha C # 5. Il compilatore fa il lavoro per iscritto per te tutta quella piastra della caldaia, e fino a quando i tempi di esecuzione di js non lo faranno, dovrai comunque passare un richiamo occasionale qua e là.

Tuttavia, potresti non voler sempre portare i callback al livello di semplicità del codice lineare - gettare le funzioni in giro non deve essere brutto, c'è un intero mondo che lavora con questo tipo di codice e rimangono sani senza asynce await.

Ad esempio, usa le funzioni di ordine superiore (il mio 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!"); }
);

2

Mi ci è voluto un po 'per decodificare perché lo stai facendo in questo modo, ma penso che questo 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();

Prima di tutto, quante più richieste AJAX possono fare simultaneamente e attende fino a quando non sono state completate tutte prima di abilitare il pulsante Start. Sarà più veloce di un'attesa sequenziale e, penso, è molto più facile da seguire.

EDIT : Nota che questo presuppone che tu abbia a .each()disposizione, e che myRepositorysia un array. Fai attenzione a quale iterazione di loop usi qui al posto di quella, se non è disponibile - questa sfrutta le proprietà di chiusura per il callback. Non sono sicuro di ciò che hai a disposizione, dal momento che LoadImageAsyncsembra far parte di una biblioteca specializzata - non vedo risultati in Google.


+1, ho a .each()disposizione e, ora che me lo dici, non è strettamente necessario eseguire il caricamento in sequenza. Proverò sicuramente la tua soluzione. (Anche se accetterò la risposta di vski, poiché è più vicina alla domanda originale e più generale.)
Heinzi,

@Heinzi Concordato su quanto sia diverso, ma (penso) questo è anche un esempio decente di come lingue diverse hanno modi diversi di gestire la stessa cosa. Se qualcosa ti sembra imbarazzante quando lo traduci in un'altra lingua, probabilmente c'è un modo più semplice per farlo usando un paradigma diverso.
Izkata,

1

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

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

1 / Utilizzo delle funzioni denominate anziché di callback anonimi

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

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

In questo 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 preferenze. A proposito, è quello che utilizza 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 utilizzo molti callback nidificati, perché è molto più leggibile quando è utilizzato molto codice.


0

In poche parole, JavaScript non ha lo zucchero sintattico di await.
Ma spostare la parte "end" nella parte inferiore della funzione è facile; e con una funzione anonima che esegue immediatamente, possiamo evitare di dichiarare un riferimento ad essa.

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

È inoltre possibile passare la parte "end" come callback di successo alla funzione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.