Comment gardez-vous le code avec les continuations / rappels lisibles?

10

Résumé: Existe-t-il des modèles de bonnes pratiques bien établis que je peux suivre pour garder mon code lisible malgré l'utilisation de code asynchrone et de rappels?


J'utilise une bibliothèque JavaScript qui fait beaucoup de choses de manière asynchrone et s'appuie fortement sur les rappels. Il semble que l'écriture d'une méthode simple "charger A, charger B, ..." devienne assez compliquée et difficile à suivre en utilisant ce modèle.

Permettez-moi de donner un exemple (artificiel). Disons que je veux charger un tas d'images (de manière asynchrone) à partir d'un serveur Web distant. En C # / async, j'écrirais quelque chose comme ceci:

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

La disposition du code suit le "flux d'événements": tout d'abord, le bouton de démarrage est désactivé, puis les images sont chargées ( awaitgarantit que l'interface utilisateur reste sensible), puis le bouton de démarrage est réactivé.

En JavaScript, en utilisant des rappels, j'ai trouvé ceci:

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

Je pense que les inconvénients sont évidents: j'ai dû retravailler la boucle en un appel récursif, le code qui devrait être exécuté à la fin est quelque part au milieu de la fonction, le code commençant le téléchargement ( loadImage(0)) est tout en bas, et c'est généralement beaucoup plus difficile à lire et à suivre. C'est moche et je n'aime pas ça.

Je suis sûr que je ne suis pas le premier à rencontrer ce problème, donc ma question est: Y a-t-il des modèles de bonnes pratiques bien établis que je peux suivre pour garder mon code lisible malgré l'utilisation de code asynchrone et de rappels?

Heinzi
la source
Y a-t-il une raison particulière pour laquelle vos appels "asynchrones" doivent être effectués séquentiellement? S'agit-il d'une version simplifiée d'un autre code?
Izkata
@Izkata: La raison est juste que je voulais être gentil avec le serveur distant (= ne pas le bombarder de centaines de requêtes simultanées). Ce n'est pas une exigence fixée dans la pierre. Oui, c'est une version simplifiée du code, LoadImageAsyncc'est en fait un appel à Ext.Ajax.requestSencha Touch.
Heinzi
1
La plupart des navigateurs ne vous permettent pas de marteler le serveur de toute façon - ils mettent simplement les demandes en file d'attente et démarrent la suivante lorsque l'une des précédentes se termine.
Izkata
Dieu! beaucoup de mauvais conseils ici. Aucune quantité de modèles de conception ne vous aidera. Regardez dans async.js , async.waterfallest votre réponse.
Salman von Abbas

Réponses:

4

Il est très peu probable que vous puissiez obtenir avec plain js le même niveau de concision et d'expressivité en travaillant avec les rappels que C # 5. Le compilateur fait le travail en écrivant tout ce passe-partout pour vous, et jusqu'à ce que les runtimes js le fassent, vous devrez toujours passer un rappel occasionnel ici et là.

Cependant, vous ne voudrez peut-être pas toujours ramener les rappels au niveau de simplicité du code linéaire - les fonctions de lancement ne doivent pas être laides, il y a un monde entier qui travaille avec ce type de code, et elles restent saines sans asyncet await.

Par exemple, utilisez des fonctions d'ordre supérieur (mes js peuvent être un peu rouillés):

// 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
la source
2

Cela m'a pris un peu pour décoder pourquoi vous le faites de cette façon, mais je pense que cela pourrait être proche de ce que vous voulez?

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

Il commence d'abord par autant de requêtes AJAX qu'il peut le faire simultanément, et il attend que toutes soient terminées avant d'activer le bouton Démarrer. Ce sera plus rapide qu'une attente séquentielle et, je pense, est beaucoup plus facile à suivre.

EDIT : Notez que cela suppose que vous avez .each()disponible, et c'est myRepositoryun tableau. Faites attention à l'itération de boucle que vous utilisez ici à la place de cela, si elle n'est pas disponible - celle-ci tire parti des propriétés de fermeture pour le rappel. Je ne suis pas sûr de ce dont vous disposez, car il LoadImageAsyncsemble faire partie d'une bibliothèque spécialisée - je ne vois aucun résultat dans Google.

Izkata
la source
+1, je l'ai à .each()disposition, et, maintenant que vous le mentionnez, il n'est pas strictement nécessaire de faire le chargement séquentiellement. Je vais certainement essayer votre solution. (Bien que j'accepte la réponse de vski, car elle est plus proche de la question originale et plus générale.)
Heinzi
@Heinzi D'accord sur la différence, mais (je pense) c'est aussi un exemple décent de la façon dont différentes langues ont différentes manières de gérer la même chose. Si quelque chose vous semble gênant lorsque vous le traduisez dans une autre langue, il existe probablement un moyen plus facile de le faire en utilisant un paradigme différent.
Izkata
1

Avertissement: cette réponse ne répond pas spécifiquement à votre problème, c'est une réponse générique à la question: "Y a-t-il des modèles de bonnes pratiques bien établis que je peux suivre pour garder mon code lisible malgré l'utilisation de code asynchrone et de rappels?"

D'après ce que je sais, il n'y a pas de modèle "bien établi" pour gérer cela. Cependant, j'ai vu deux types de méthodes utilisées pour éviter les cauchemars de rappels imbriqués.

1 / Utiliser des fonctions nommées au lieu de rappels anonymes

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

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

De cette façon, vous évitez l'imbrication en envoyant la logique de la fonction anonyme dans une autre fonction.

2 / Utilisation d'une bibliothèque de gestion de flux. J'aime utiliser Step , mais c'est juste une question de préférence. C'est celui que LinkedIn utilise d'ailleurs.

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

J'utilise une bibliothèque de gestion de flux lorsque j'utilise beaucoup de rappels imbriqués, car elle est beaucoup plus lisible lorsqu'il y a beaucoup de code l'utilisant.

Florian Margaine
la source
0

Autrement dit, JavaScript n'a pas le sucre syntaxique de await.
Mais déplacer la partie "fin" dans le bas de la fonction est facile; et avec une fonction anonyme exécutant immédiatement, nous pouvons éviter de lui déclarer une référence.

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

Vous pouvez également passer la partie "fin" comme rappel de réussite à la fonction.

Gipsy King
la source