157 votes

Comment éviter l'imbrication longue des fonctions asynchrones dans Node.js

Je veux créer une page qui affiche des données provenant d'une base de données. J'ai donc créé des fonctions qui récupèrent ces données dans ma base de données. Je suis un débutant en Node.js, donc d'après ce que je comprends, si je veux les utiliser toutes dans une seule page (réponse HTTP), je dois les imbriquer toutes :

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 de ce type, alors l'emboîtement 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.

73voto

Daniel Vassallo Points 142049

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

Les suivantes :

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

Il pourrait être réécrit pour ressembler à quelque chose comme 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);

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

En outre, notez que comme @pst 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 directe. Dans ce cas, l'utilisation de fonctions anonymes en ligne est encore plus préférable.

63voto

Baggz Points 6836

Kay, il suffit d'utiliser l'un de ces modules.

Il va tourner ça :

dbGet('userIdOf:bobvance', function(userId) {
    dbSet('user:' + userId + ':email', 'bobvance@potato.egg', function() {
        dbSet('user:' + userId + ':firstName', 'Bob', function() {
            dbSet('user:' + userId + ':lastName', 'Vance', function() {
                okWeAreDone();
            });
        });
    });
});

Dans ça :

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

    },function(userId) {
        dbSet('user:' + userId + ':email', 'bobvance@potato.egg', this.MULTI());
        dbSet('user:' + userId + ':firstName', 'Bob', this.MULTI());
        dbSet('user:' + userId + ':lastName', 'Vance', this.MULTI());

    },function() {
        okWeAreDone()
    }
);

18voto

Caolan Points 1736

Pour l'essentiel, je suis d'accord avec Daniel Vassallo. Si vous pouvez décomposer une fonction complexe et profondément imbriquée en fonctions nommées distinctes, c'est généralement une bonne idée. Dans les cas où il est plus judicieux de le faire à l'intérieur d'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 d'aborder ce problème, alors jetez un coup d'œil à la page des modules node.js et voyez ce que vous en pensez.

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

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

L'avantage de cette approche est que vous pouvez rapidement modifier votre code pour récupérer les données en parallèle en remplaçant la fonction "series" par "parallel". De plus, async.js fonctionnera également fonctionnera également dans le navigateur, de sorte que vous pourrez utiliser les mêmes méthodes que dans node.js si vous rencontrez un code asynchrone délicat.

J'espère que cela vous sera utile !

18voto

Guido Points 101

Vous pouvez utiliser cette astuce avec un tableau plutôt qu'avec 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 parallèles de processus :

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

11voto

galambalazs Points 24393

Ce dont vous avez besoin, c'est d'un peu de sucre syntaxique. Regardez ça :

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

Pretty soigné n'est-ce pas ? Vous avez peut-être remarqué que html est devenu un tableau. C'est en partie parce que les chaînes de caractères sont immuables, et qu'il est donc préférable de mettre en mémoire tampon votre sortie dans un tableau, plutôt que de jeter des chaînes de caractères de plus en plus grandes. L'autre raison est l'existence d'une autre syntaxe intéressante avec bind .

Queue dans l'exemple est vraiment juste un exemple et avec partial peut être mis en œuvre 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);
  }
};

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X