Comment éviter une longue imbrication de fonctions asynchrones dans Node.js

158

Je veux créer une page qui affiche certaines données d'une base de données, j'ai donc créé des fonctions qui récupèrent ces données de ma base de données. Je ne suis qu'un débutant dans Node.js, donc pour autant que je sache, si je veux tous les utiliser dans une seule page (réponse HTTP), je devrais tous les imbriquer:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  getSomeDate(client, function(someData) {
    html += "<p>"+ someData +"</p>";
    getSomeOtherDate(client, function(someOtherData) {
      html += "<p>"+ someOtherData +"</p>";
      getMoreData(client, function(moreData) {
        html += "<p>"+ moreData +"</p>";
        res.write(html);
        res.end();
      });
    });
  });

S'il y a beaucoup de fonctions comme ça, alors l'imbrication devient un problème .

Y a-t-il un moyen d'éviter cela? Je suppose que cela a à voir avec la façon dont vous combinez plusieurs fonctions asynchrones, ce qui semble être quelque chose de fondamental.

Kay Pale
la source
12
Donc, lorsque vous avez 10 fonctions asynchrones, vous avez 10 niveaux d'indentation?
Kay Pale
Ce lien peut vous aider. stackoverflow.com/a/4631909/290340
Evan Plaice
1
Un autre problème: insérer une autre fonction entre getSomeDateet getSomeOtherDatefinit par changer l'indentation de nombreuses lignes, ce qui rend l'historique de git plus difficile à lire ( git blameest même inutile après cela), et vous faites probablement des bugs en faisant cela manuellement
Daniel Alder

Réponses:

73

Observation intéressante. Notez qu'en JavaScript, vous pouvez normalement remplacer les fonctions de rappel anonyme en ligne par des variables de fonction nommées.

Le suivant:

http.createServer(function (req, res) {
   // inline callback function ...

   getSomeData(client, function (someData) {
      // another inline callback function ...

      getMoreData(client, function(moreData) {
         // one more inline callback function ...
      });
   });

   // etc ...
});

Peut être réécrit pour ressembler à ceci:

var moreDataParser = function (moreData) {
   // date parsing logic
};

var someDataParser = function (someData) {
   // some data parsing logic

   getMoreData(client, moreDataParser);
};

var createServerCallback = function (req, res) {
   // create server logic

   getSomeData(client, someDataParser);

   // etc ...
};

http.createServer(createServerCallback);

Cependant, à moins que vous ne prévoyiez de réutiliser la logique de rappel à d'autres endroits, il est souvent beaucoup plus facile de lire des fonctions anonymes en ligne, comme dans votre exemple. Cela vous évitera également d'avoir à trouver un nom pour tous les rappels.

De plus, notez que comme @pst l'a noté dans un commentaire ci-dessous, si vous accédez à des variables de fermeture dans les fonctions internes, ce qui précède ne serait pas une traduction simple. Dans de tels cas, l'utilisation de fonctions anonymes en ligne est encore plus préférable.

Daniel Vassallo
la source
26
Cependant, (et vraiment juste pour comprendre le compromis) lorsqu'elle n'est pas imbriquée, certaines sémantiques de fermeture sur les variables peuvent être perdues, ce n'est donc pas une traduction directe. Dans l'exemple ci-dessus, l'accès à «res» getMoreDataest perdu.
2
Je pense que votre solution est cassée: someDataParseranalyse en fait TOUTES les données, car il appelle également getMoreData. En ce sens, le nom de la fonction est incorrect et il devient évident que nous n'avons pas réellement supprimé le problème d'imbrication.
Konstantin Schubert
63

Kay, utilisez simplement l'un de ces modules.

Cela transformera ceci:

dbGet('userIdOf:bobvance', function(userId) {
    dbSet('user:' + userId + ':email', '[email protected]', function() {
        dbSet('user:' + userId + ':firstName', 'Bob', function() {
            dbSet('user:' + userId + ':lastName', 'Vance', function() {
                okWeAreDone();
            });
        });
    });
});

Dans ceci:

flow.exec(
    function() {
        dbGet('userIdOf:bobvance', this);

    },function(userId) {
        dbSet('user:' + userId + ':email', '[email protected]', this.MULTI());
        dbSet('user:' + userId + ':firstName', 'Bob', this.MULTI());
        dbSet('user:' + userId + ':lastName', 'Vance', this.MULTI());

    },function() {
        okWeAreDone()
    }
);
Baggz
la source
9
J'ai jeté un coup d'œil rapide à flow-js, step et async et il semble qu'ils ne traitent que de l'ordre d'exécution des fonctions. Dans mon cas, il y a un accès aux variables de fermeture en ligne dans chaque indentation. Ainsi, par exemple, les fonctions fonctionnent comme ceci: obtenir HTTP req / res, obtenir l'ID utilisateur de la base de données pour le cookie, obtenir un e-mail pour l'ID utilisateur le plus récent, obtenir plus de données pour l'e-mail ultérieur, ..., obtenir X pour plus tard Y, ... Si je ne me trompe pas, ces frameworks assurent seulement que les fonctions asynchrones seront exécutées dans le bon ordre, mais dans chaque corps de fonction, il n'y a pas moyen d'obtenir la variable fournie naturellement par les fermetures (?) Merci :)
Kay Pale
9
En termes de classement de ces bibliothèques, j'ai vérifié le nombre de "Stars" sur chacune d'elles sur Github. async a le plus avec environ 3000, Step est le suivant avec environ 1000, les autres sont nettement moins. Bien sûr, ils ne font pas tous la même chose :-)
kgilpin
3
@KayPale J'ai tendance à utiliser async.waterfall, et j'aurai parfois mes propres fonctions pour chaque étape / étape qui transmettront ce dont la prochaine étape a besoin, ou définir des variables avant l'appel async.METHOD afin qu'il soit disponible en aval. J'utiliserai également METHODNAME.bind (...) pour mes appels async. *, Ce qui fonctionne plutôt bien aussi.
Tracker1
Une petite question: dans votre liste de modules, les deux derniers sont-ils identiques? Ie "async.js" et "async"
dari0h
18

Pour la plupart, je suis d'accord avec Daniel Vassallo. Si vous pouvez diviser une fonction compliquée et profondément imbriquée en fonctions nommées distinctes, c'est généralement une bonne idée. Pour les moments où cela a du sens de le faire dans une seule fonction, vous pouvez utiliser l'une des nombreuses bibliothèques asynchrones node.js disponibles. Les gens ont trouvé de nombreuses façons différentes de résoudre ce problème, alors jetez un œil à la page des modules node.js et voyez ce que vous en pensez.

J'ai écrit un module pour cela moi-même, appelé async.js . En utilisant cela, l'exemple ci-dessus pourrait être mis à jour pour:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  async.series({
    someData: async.apply(getSomeDate, client),
    someOtherData: async.apply(getSomeOtherDate, client),
    moreData: async.apply(getMoreData, client)
  },
  function (err, results) {
    var html = "<h1>Demo page</h1>";
    html += "<p>" + results.someData + "</p>";
    html += "<p>" + results.someOtherData + "</p>";
    html += "<p>" + results.moreData + "</p>";
    res.write(html);
    res.end();
  });
});

Une bonne chose à propos de cette approche est que vous pouvez rapidement changer votre code pour récupérer les données en parallèle en changeant la fonction «série» en «parallèle». De plus, async.js fonctionnera également dans le navigateur, vous pouvez donc utiliser les mêmes méthodes que vous le feriez dans node.js si vous rencontrez un code asynchrone délicat.

J'espère que c'est utile!

Caolan
la source
Salut Caolan et merci pour la réponse! Dans mon cas, il y a un accès aux variables de fermeture en ligne dans chaque indentation. Ainsi, par exemple, les fonctions fonctionnent comme ceci: obtenir HTTP req / res, obtenir l'ID utilisateur de la base de données pour le cookie, obtenir un e-mail pour l'ID utilisateur le plus récent, obtenir plus de données pour l'e-mail ultérieur, ..., obtenir X pour plus tard Y, ... Si je ne me trompe pas, le code que vous suggérez assure seulement que les fonctions asynchrones seront exécutées dans le bon ordre, mais dans chaque corps de fonction, il n'y a pas moyen d'obtenir la variable fournie naturellement par les fermetures dans mon code d'origine. Est-ce le cas?
Kay Pale
3
Ce que vous essayez de réaliser est appelé sur le plan architectural un pipeline de données. Vous pouvez utiliser la cascade asynchrone pour de tels cas.
Rudolf Meijering
18

Vous pouvez utiliser cette astuce avec un tableau plutôt que des fonctions imbriquées ou un module.

Beaucoup plus facile pour les yeux.

var fs = require("fs");
var chain = [
    function() { 
        console.log("step1");
        fs.stat("f1.js",chain.shift());
    },
    function(err, stats) {
        console.log("step2");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step3");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step4");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step5");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("done");
    },
];
chain.shift()();

Vous pouvez étendre l'idiome pour des processus parallèles ou même des chaînes de processus parallèles:

var fs = require("fs");
var fork1 = 2, fork2 = 2, chain = [
    function() { 
        console.log("step1");
        fs.stat("f1.js",chain.shift());
    },
    function(err, stats) {
        console.log("step2");
        var next = chain.shift();
        fs.stat("f2a.js",next);
        fs.stat("f2b.js",next);
    },
    function(err, stats) {
        if ( --fork1 )
            return;
        console.log("step3");
        var next = chain.shift();

        var chain1 = [
            function() { 
                console.log("step4aa");
                fs.stat("f1.js",chain1.shift());
            },
            function(err, stats) { 
                console.log("step4ab");
                fs.stat("f1ab.js",next);
            },
        ];
        chain1.shift()();

        var chain2 = [
            function() { 
                console.log("step4ba");
                fs.stat("f1.js",chain2.shift());
            },
            function(err, stats) { 
                console.log("step4bb");
                fs.stat("f1ab.js",next);
            },
        ];
        chain2.shift()();
    },
    function(err, stats) {
        if ( --fork2 )
            return;
        console.log("done");
    },
];
chain.shift()();
Guido
la source
15

J'aime beaucoup async.js à cet effet.

Le problème est résolu par la commande waterfall:

cascade (tâches, [rappel])

Exécute un tableau de fonctions en série, chacune transmettant ses résultats à la suivante du tableau. Cependant, si l'une des fonctions transmet une erreur au rappel, la fonction suivante n'est pas exécutée et le rappel principal est immédiatement appelé avec l'erreur.

Arguments

tâches - Un tableau de fonctions à exécuter, chaque fonction reçoit un rappel (err, résultat1, résultat2, ...) qu'elle doit appeler à la fin. Le premier argument est une erreur (qui peut être nulle) et tous les autres arguments seront passés comme arguments pour la tâche suivante. callback (err, [results]) - Un callback optionnel à exécuter une fois que toutes les fonctions sont terminées. Ce sera passé les résultats du rappel de la dernière tâche.

Exemple

async.waterfall([
    function(callback){
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback){
        callback(null, 'three');
    },
    function(arg1, callback){
        // arg1 now equals 'three'
        callback(null, 'done');
    }
], function (err, result) {
    // result now equals 'done'    
});

Quant aux variables req, res, elles seront partagées dans la même portée que function (req, res) {} qui renfermait tout l'appel async.waterfall.

Non seulement ainsi, async est très propre. Ce que je veux dire, c'est que je change beaucoup de cas comme celui-ci:

function(o,cb){
    function2(o,function(err, resp){
        cb(err,resp);
    })
}

Au premier:

function(o,cb){
    function2(o,cb);
}

Puis à ceci:

function2(o,cb);

Puis à ceci:

async.waterfall([function2,function3,function4],optionalcb)

Il permet également d'appeler très rapidement de nombreuses fonctions prédéfinies préparées pour async depuis util.js. Enchaînez simplement ce que vous voulez faire, assurez-vous que o, cb est universellement géré. Cela accélère beaucoup l'ensemble du processus de codage.

Grant Li
la source
11

Il vous faut un peu de sucre syntaxique. Vérifiez ceci:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = ["<h1>Demo page</h1>"];
  var pushHTML = html.push.bind(html);

  Queue.push( getSomeData.partial(client, pushHTML) );
  Queue.push( getSomeOtherData.partial(client, pushHTML) );
  Queue.push( getMoreData.partial(client, pushHTML) );
  Queue.push( function() {
    res.write(html.join(''));
    res.end();
  });
  Queue.execute();
}); 

Assez bien , n'est-ce pas? Vous remarquerez peut-être que html est devenu un tableau. C'est en partie parce que les chaînes sont immuables, il est donc préférable de mettre en mémoire tampon votre sortie dans un tableau, que de supprimer des chaînes de plus en plus grandes. L'autre raison est à cause d'une autre syntaxe intéressante avec bind.

Queuedans l'exemple n'est en réalité qu'un exemple et partialpeut être implémenté comme suit

// Functional programming for the rescue
Function.prototype.partial = function() {
  var fun = this,
      preArgs = Array.prototype.slice.call(arguments);
  return function() {
    fun.apply(null, preArgs.concat.apply(preArgs, arguments));
  };
};

Queue = [];
Queue.execute = function () {
  if (Queue.length) {
    Queue.shift()(Queue.execute);
  }
};
gblazex
la source
1
Queue.execute () exécutera simplement les partiels l'un après l'autre, sans attendre les résultats des appels asynchrones.
ngn
Spot on, merci. J'ai mis à jour la réponse. Voici un test: jsbin.com/ebobo5/edit (avec une lastfonction optionnelle )
gblazex
Salut galambalazs et merci pour la réponse! Dans mon cas, il y a un accès aux variables de fermeture en ligne dans chaque indentation. Ainsi, par exemple, les fonctions fonctionnent comme ceci: obtenir HTTP req / res, obtenir l'ID utilisateur de la base de données pour le cookie, obtenir un e-mail pour l'ID utilisateur le plus récent, obtenir plus de données pour l'e-mail ultérieur, ..., obtenir X pour plus tard Y, ... Si je ne me trompe pas, le code que vous suggérez assure seulement que les fonctions asynchrones seront exécutées dans le bon ordre, mais dans chaque corps de fonction, il n'y a pas moyen d'obtenir la variable fournie naturellement par les fermetures dans mon code d'origine. Est-ce le cas?
Kay Pale
1
Eh bien, vous perdez définitivement des fermetures dans toutes les réponses. Ce que vous pouvez faire, c'est créer un objet dans la portée globale des données partagées . Ainsi, par exemple, votre première fonction ajoute obj.emailet votre fonction suivante utilise obj.emailpuis la supprime (ou simplement l'assigne null).
gblazex
7

Je suis amoureux d' Async.js depuis que je l'ai trouvé. Il a une async.seriesfonction que vous pouvez utiliser pour éviter une longue imbrication.

Documentation:-


série (tâches, [rappel])

Exécutez un tableau de fonctions en série, chacune s'exécutant une fois la fonction précédente terminée. [...]

Arguments

tasks- Un tableau de fonctions à exécuter, chaque fonction reçoit un rappel qu'elle doit appeler à la fin. callback(err, [results])- Un rappel optionnel à exécuter une fois que toutes les fonctions sont terminées. Cette fonction obtient un tableau de tous les arguments passés aux callbacks utilisés dans le tableau.


Voici comment nous pouvons l'appliquer à votre exemple de code: -

http.createServer(function (req, res) {

    res.writeHead(200, {'Content-Type': 'text/html'});

    var html = "<h1>Demo page</h1>";

    async.series([
        function (callback) {
            getSomeData(client, function (someData) { 
                html += "<p>"+ someData +"</p>";

                callback();
            });
        },

        function (callback) {
            getSomeOtherData(client, function (someOtherData) { 
                html += "<p>"+ someOtherData +"</p>";

                callback(); 
            });
        },

        funciton (callback) {
            getMoreData(client, function (moreData) {
                html += "<p>"+ moreData +"</p>";

                callback();
            });
        }
    ], function () {
        res.write(html);
        res.end();
    });
});
Salman von Abbas
la source
6

Le sucre syntaxique le plus simple que j'ai vu est la promesse de nœud.

npm install node-promise || git clone https://github.com/kriszyp/node-promise

En utilisant cela, vous pouvez enchaîner les méthodes asynchrones comme:

firstMethod().then(secondMethod).then(thirdMethod);

La valeur de retour de chacun est disponible comme argument dans le suivant.

Nikhil Ranjan
la source
3

Ce que vous avez fait là-bas, c'est de prendre un modèle asynchrone et de l'appliquer à 3 fonctions appelées en séquence, chacune attendant que la précédente se termine avant de commencer - c'est-à-dire que vous les avez rendues synchrones . Le point sur la programmation asynchrone est que vous pouvez exécuter plusieurs fonctions toutes en même temps et ne pas avoir à attendre qu'elles soient terminées.

si getSomeDate () ne fournit rien à getSomeOtherDate (), qui ne fournit rien à getMoreData () alors pourquoi ne les appelez-vous pas de manière asynchrone comme js le permet ou s'ils sont interdépendants (et non asynchrones), écrivez-les comme un fonction unique?

Vous n'avez pas besoin d'utiliser l'imbrication pour contrôler le flux - par exemple, terminez chaque fonction en appelant une fonction commune qui détermine quand les 3 sont terminées, puis envoie la réponse.

Nick Tulett
la source
2

Supposons que vous puissiez faire ceci:

http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});
    var html = "<h1>Demo page</h1>";
    chain([
        function (next) {
            getSomeDate(client, next);
        },
        function (next, someData) {
            html += "<p>"+ someData +"</p>";
            getSomeOtherDate(client, next);
        },
        function (next, someOtherData) {
            html += "<p>"+ someOtherData +"</p>";
            getMoreData(client, next);
        },
        function (next, moreData) {
            html += "<p>"+ moreData +"</p>";
            res.write(html);
            res.end();
        }
    ]);
});

Il vous suffit d'implémenter chain () pour qu'il applique partiellement chaque fonction à la suivante et n'appelle immédiatement que la première fonction:

function chain(fs) {
    var f = function () {};
    for (var i = fs.length - 1; i >= 0; i--) {
        f = fs[i].partial(f);
    }
    f();
}
ngn
la source
Salut ngn et merci pour la réponse! Dans mon cas, il y a un accès aux variables de fermeture en ligne dans chaque indentation. Ainsi, par exemple, les fonctions fonctionnent comme ceci: obtenir HTTP req / res, obtenir l'ID utilisateur de la base de données pour le cookie, obtenir un e-mail pour l'ID utilisateur le plus récent, obtenir plus de données pour l'e-mail ultérieur, ..., obtenir X pour plus tard Y, ... Si je ne me trompe pas, le code que vous suggérez assure seulement que les fonctions asynchrones seront exécutées dans le bon ordre, mais dans chaque corps de fonction, il n'y a pas moyen d'obtenir la variable fournie naturellement par les fermetures dans mon code d'origine. Est-ce le cas?
Kay Pale
2

L'enfer des rappels peut être facilement évité en javascript pur avec fermeture. la solution ci-dessous suppose que tous les rappels suivent la signature de la fonction (erreur, données).

http.createServer(function (req, res) {
  var modeNext, onNext;

  // closure variable to keep track of next-callback-state
  modeNext = 0;

  // next-callback-handler
  onNext = function (error, data) {
    if (error) {
      modeNext = Infinity;
    } else {
      modeNext += 1;
    }
    switch (modeNext) {

    case 0:
      res.writeHead(200, {'Content-Type': 'text/html'});
      var html = "<h1>Demo page</h1>";
      getSomeDate(client, onNext);
      break;

    // handle someData
    case 1:
        html += "<p>"+ data +"</p>";
        getSomeOtherDate(client, onNext);
        break;

    // handle someOtherData
    case 2:
      html += "<p>"+ data +"</p>";
      getMoreData(client, onNext);
      break;

    // handle moreData
    case 3:
      html += "<p>"+ data +"</p>";
      res.write(html);
      res.end();
      break;

    // general catch-all error-handler
    default:
      res.statusCode = 500;
      res.end(error.message + '\n' + error.stack);
    }
  };
  onNext();
});
kai zhu
la source
1

J'ai récemment créé une abstraction plus simple appelée wait.for pour appeler les fonctions asynchrones en mode sync (basé sur Fibers). C'est à un stade précoce mais ça marche. C'est a:

https://github.com/luciotato/waitfor

En utilisant wait.for , vous pouvez appeler n'importe quelle fonction async standard de nodejs, comme s'il s'agissait d'une fonction de synchronisation.

en utilisant wait.for votre code pourrait être:

var http=require('http');
var wait=require('wait.for');

http.createServer(function(req, res) {
  wait.launchFiber(handleRequest,req, res); //run in a Fiber, keep node spinning
}).listen(8080);


//in a fiber
function handleRequest(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  var someData = wait.for(getSomeDate,client);
  html += "<p>"+ someData +"</p>";
  var someOtherData = wait.for(getSomeOtherDate,client);
  html += "<p>"+ someOtherData +"</p>";
  var moreData = wait.for(getMoreData,client);
  html += "<p>"+ moreData +"</p>";
  res.write(html);
  res.end();
};

... ou si vous voulez être moins verbeux (et ajouter également une détection d'erreur)

//in a fiber
function handleRequest(req, res) {
  try {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write(
    "<h1>Demo page</h1>" 
    + "<p>"+ wait.for(getSomeDate,client) +"</p>"
    + "<p>"+ wait.for(getSomeOtherDate,client) +"</p>"
    + "<p>"+ wait.for(getMoreData,client) +"</p>"
    );
    res.end();
  }
  catch(err) {
   res.end('error '+e.message); 
  }

};

Dans tous les cas, getSomeDate , getSomeOtherDate et getMoreData doivent être des fonctions asynchrones standard avec le dernier paramètre un rappel de fonction (err, data)

un péché:

function getMoreData(client, callback){
  db.execute('select moredata from thedata where client_id=?',[client.id],
       ,function(err,data){
          if (err) callback(err);
          callback (null,data);
        });
}
Lucio M. Tato
la source
1

Pour résoudre ce problème, j'ai écrit nodent ( https://npmjs.org/package/nodent ) qui pré-traite votre JS de manière invisible. Votre exemple de code deviendrait (asynchrone, vraiment - lisez la documentation).

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  someData <<= getSomeDate(client) ;

  html += "<p>"+ someData +"</p>";
  someOtherData <<= getSomeOtherDate(client) ;

  html += "<p>"+ someOtherData +"</p>";
  moreData <<= getMoreData(client) ;

  html += "<p>"+ moreData +"</p>";
  res.write(html);
  res.end();
});

De toute évidence, il existe de nombreuses autres solutions, mais le prétraitement a l'avantage d'avoir peu ou pas de temps d'exécution et grâce à la prise en charge de la carte source, il est également facile à déboguer.

MatAtBread
la source
0

J'ai eu le même problème. J'ai vu les principales bibliothèques à nœuds exécuter des fonctions asynchrones, et elles présentent un chaînage si non naturel (vous devez utiliser trois méthodes ou plus confs, etc.) pour construire votre code.

J'ai passé quelques semaines à développer une solution simple et facile à lire. S'il vous plaît, essayez EnqJS . Toutes les opinions seront appréciées.

Au lieu de:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  getSomeDate(client, function(someData) {
    html += "<p>"+ someData +"</p>";
    getSomeOtherDate(client, function(someOtherData) {
      html += "<p>"+ someOtherData +"</p>";
      getMoreData(client, function(moreData) {
        html += "<p>"+ moreData +"</p>";
        res.write(html);
        res.end();
      });
    });
  });

avec EnqJS:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";

  enq(function(){
    var self=this;
    getSomeDate(client, function(someData){
      html += "<p>"+ someData +"</p>";
      self.return();
    })
  })(function(){
    var self=this;
    getSomeOtherDate(client, function(someOtherData){ 
      html += "<p>"+ someOtherData +"</p>";
      self.return();
    })
  })(function(){
    var self=this;
    getMoreData(client, function(moreData) {
      html += "<p>"+ moreData +"</p>";
      self.return();
      res.write(html);
      res.end();
    });
  });
});

Observez que le code semble être plus gros qu'avant. Mais ce n'est pas imbriqué comme avant. Pour paraître plus naturelles, les chaînes sont appelées immédiatement:

enq(fn1)(fn2)(fn3)(fn4)(fn4)(...)

Et pour dire qu'il est revenu, à l'intérieur de la fonction que nous appelons:

this.return(response)
Thadeu de Paula
la source
0

Je le fais d'une manière assez primitive mais efficace. Par exemple, je dois obtenir un modèle avec ses parents et ses enfants et disons que je dois faire des requêtes séparées pour eux:

var getWithParents = function(id, next) {
  var getChildren = function(model, next) {
        /*... code ... */
        return next.pop()(model, next);
      },
      getParents = function(model, next) {
        /*... code ... */
        return next.pop()(model, next);
      }
      getModel = function(id, next) {
        /*... code ... */
        if (model) {
          // return next callbacl
          return next.pop()(model, next);
        } else {
          // return last callback
          return next.shift()(null, next);
        }
      }

  return getModel(id, [getParents, getChildren, next]);
}
mvbl fst
la source
0

Utilisez Fibers https://github.com/laverdet/node-fibers, cela rend le code asynchrone synchrone (sans blocage)

J'utilise personnellement ce petit wrapper http://alexeypetrushin.github.com/synchronize Exemple de code de mon projet (chaque méthode est en fait asynchrone, travaillant avec un fichier asynchrone IO) J'ai même peur d'imaginer quel désordre ce serait avec le rappel ou bibliothèques d'assistance async-control-flow.

_update: (version, changesBasePath, changes, oldSite) ->
  @log 'updating...'
  @_updateIndex version, changes
  @_updateFiles version, changesBasePath, changes
  @_updateFilesIndexes version, changes
  configChanged = @_updateConfig version, changes
  @_updateModules version, changes, oldSite, configChanged
  @_saveIndex version
  @log "updated to #{version} version"
Alexey Petrushin
la source
0

Task.js vous propose ceci:

spawn(function*() {
    try {
        var [foo, bar] = yield join(read("foo.json"),
                                    read("bar.json")).timeout(1000);
        render(foo);
        render(bar);
    } catch (e) {
        console.log("read failed: " + e);
    }
});

Au lieu de cela:

var foo, bar;
var tid = setTimeout(function() { failure(new Error("timed out")) }, 1000);

var xhr1 = makeXHR("foo.json",
                   function(txt) { foo = txt; success() },
                   function(err) { failure() });
var xhr2 = makeXHR("bar.json",
                   function(txt) { bar = txt; success() },
                   function(e) { failure(e) });

function success() {
    if (typeof foo === "string" && typeof bar === "string") {
        cancelTimeout(tid);
        xhr1 = xhr2 = null;
        render(foo);
        render(bar);
    }
}

function failure(e) {
    xhr1 && xhr1.abort();
    xhr1 = null;
    xhr2 && xhr2.abort();
    xhr2 = null;
    console.log("read failed: " + e);
}
Janus Troelsen
la source
0

Après que les autres ont répondu, vous avez déclaré que votre problème était des variables locales. Il semble qu'un moyen facile de le faire est d'écrire une fonction externe pour contenir ces variables locales, puis d'utiliser un tas de fonctions internes nommées et d'y accéder par leur nom. De cette façon, vous n'en nicherez que deux en profondeur, quel que soit le nombre de fonctions dont vous avez besoin pour enchaîner.

Voici la tentative de mon débutant d'utiliser le mysqlmodule Node.js avec l'imbrication:

function with_connection(sql, bindings, cb) {
    pool.getConnection(function(err, conn) {
        if (err) {
            console.log("Error in with_connection (getConnection): " + JSON.stringify(err));
            cb(true);
            return;
        }
        conn.query(sql, bindings, function(err, results) {
            if (err) {
                console.log("Error in with_connection (query): " + JSON.stringify(err));
                cb(true);
                return;
            }
            console.log("with_connection results: " + JSON.stringify(results));
            cb(false, results);
        });
    });
}

Ce qui suit est une réécriture utilisant des fonctions internes nommées. La fonction externe with_connectionpeut également être utilisée comme support pour les variables locales. (Ici, j'ai les paramètres sql, bindings, cbqui agissent de la même manière, mais vous pouvez définir quelques - unes des variables locales supplémentaires with_connection.)

function with_connection(sql, bindings, cb) {

    function getConnectionCb(err, conn) {
        if (err) {
            console.log("Error in with_connection/getConnectionCb: " + JSON.stringify(err));
            cb(true);
            return;
        }
        conn.query(sql, bindings, queryCb);
    }

    function queryCb(err, results) {
        if (err) {
            console.log("Error in with_connection/queryCb: " + JSON.stringify(err));
            cb(true);
            return;
        }
        cb(false, results);
    }

    pool.getConnection(getConnectionCb);
}

J'avais pensé qu'il serait peut-être possible de créer un objet avec des variables d'instance et d'utiliser ces variables d'instance en remplacement des variables locales. Mais maintenant, je trouve que l'approche ci-dessus utilisant des fonctions imbriquées et des variables locales est plus simple et plus facile à comprendre. Il faut du temps pour désapprendre OO, il semble :-)

Voici donc ma version précédente avec un objet et des variables d'instance.

function DbConnection(sql, bindings, cb) {
    this.sql = sql;
    this.bindings = bindings;
    this.cb = cb;
}
DbConnection.prototype.getConnection = function(err, conn) {
    var self = this;
    if (err) {
        console.log("Error in DbConnection.getConnection: " + JSON.stringify(err));
        this.cb(true);
        return;
    }
    conn.query(this.sql, this.bindings, function(err, results) { self.query(err, results); });
}
DbConnection.prototype.query = function(err, results) {
    var self = this;
    if (err) {
        console.log("Error in DbConnection.query: " + JSON.stringify(err));
        self.cb(true);
        return;
    }
    console.log("DbConnection results: " + JSON.stringify(results));
    self.cb(false, results);
}

function with_connection(sql, bindings, cb) {
    var dbc = new DbConnection(sql, bindings, cb);
    pool.getConnection(function (err, conn) { dbc.getConnection(err, conn); });
}

Il s'avère que cela bindpeut être utilisé à un certain avantage. Cela me permet de me débarrasser des fonctions anonymes quelque peu laides que j'ai créées et qui n'ont rien fait, sauf pour se transmettre à un appel de méthode. Je n'ai pas pu passer la méthode directement car elle aurait été impliquée avec la mauvaise valeur de this. Mais avec bind, je peux spécifier la valeur de ce thisque je veux.

function DbConnection(sql, bindings, cb) {
    this.sql = sql;
    this.bindings = bindings;
    this.cb = cb;
}
DbConnection.prototype.getConnection = function(err, conn) {
    var f = this.query.bind(this);
    if (err) {
        console.log("Error in DbConnection.getConnection: " + JSON.stringify(err));
        this.cb(true);
        return;
    }
    conn.query(this.sql, this.bindings, f);
}
DbConnection.prototype.query = function(err, results) {
    if (err) {
        console.log("Error in DbConnection.query: " + JSON.stringify(err));
        this.cb(true);
        return;
    }
    console.log("DbConnection results: " + JSON.stringify(results));
    this.cb(false, results);
}

// Get a connection from the pool, execute `sql` in it
// with the given `bindings`.  Invoke `cb(true)` on error,
// invoke `cb(false, results)` on success.  Here,
// `results` is an array of results from the query.
function with_connection(sql, bindings, cb) {
    var dbc = new DbConnection(sql, bindings, cb);
    var f = dbc.getConnection.bind(dbc);
    pool.getConnection(f);
}

Bien sûr, rien de tout cela n'est correct JS avec le codage Node.js - je viens de passer quelques heures dessus. Mais peut-être qu'avec un peu de polissage, cette technique peut vous aider?

hibbelig
la source
0

Si vous ne voulez pas utiliser "step" ou "seq", essayez "line" qui est une fonction simple pour réduire les rappels asynchrones imbriqués.

https://github.com/kevin0571/node-line

Kevin
la source
0

En utilisant le fil, votre code ressemblerait à ceci:

http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});

    var l = new Wire();

    getSomeDate(client, l.branch('someData'));
    getSomeOtherDate(client, l.branch('someOtherData'));
    getMoreData(client, l.branch('moreData'));

    l.success(function(r) {
        res.write("<h1>Demo page</h1>"+
            "<p>"+ r['someData'] +"</p>"+
            "<p>"+ r['someOtherData'] +"</p>"+
            "<p>"+ r['moreData'] +"</p>");
        res.end();
    });
});
Daniel Garmoshka
la source
0

pour votre connaissance, pensez à Jazz.js https://github.com/Javanile/Jazz.js/wiki/Script-showcase

    const jj = require ('jazz.js');

    // pile ultra-compat
    jj.script ([
        a => ProcessTaskOneCallbackAtEnd (a),
        b => ProcessTaskTwoCallbackAtEnd (b),
        c => ProcessTaskThreeCallbackAtEnd (c),
        d => ProcessTaskFourCallbackAtEnd (d),
        e => ProcessTaskFiveCallbackAtEnd (e),
    ]);

cicciodarkast
la source