【问题标题】:Refactoring nested callbacks, node.js, async重构嵌套回调、node.js、异步
【发布时间】:2012-07-15 21:42:11
【问题描述】:
function indexArticles(callback) {
  fs.readdir("posts/", function(err, files) {
    async.map(files, readPost, function(err, markdown) {
      async.map(markdown, parse, function(err, results) {
        async.sortBy(results, function(obj, callback) {
          callback(err, obj.date);
        }, function(err, sorted) {
          callback( {"articles": sorted.reverse()} );
        });
      });
    });
  });
}

我正试图弄清楚如何让这个更漂亮——正如你所知道的,我正在使用 caolan 的异步库,但我不确定要使用哪个控制流结构。例如,如果我使用 async.waterfall 似乎会产生更多代码,每个步骤都必须包装在一个匿名函数中。例如,这只是带有瀑布的嵌套版本的前两行:

function indexArticles(callback) {
  async.waterfall([
    function(callback) {
      fs.readdir("posts/", function(err, files) {
        callback(err, files)
      })
    },

    function(files, callback) {
      async.map(files, readPost, function(err, markdown) {
        callback(err, markdown)
      })
    }])
}

您将如何改进?

如果有一种方法不仅可以从左侧部分应用参数,那么我可以看到这样做,例如,

function indexArticles(callback) {
  async.waterfall([
    async.apply(fs.readdir, "posts/"),
    async.apply(async.map, __, readPost),
    async.apply(async.map, __, parse),
    // etc...
  ])
}

【问题讨论】:

  • 虽然waterfall 最终可能会包含更多字符,但我认为它最终会更具可读性。另请查看apply 以帮助处理所有这些匿名函数。
  • 你能看一下我刚刚发布的瀑布示例并告诉我我做得对吗?

标签: node.js asynchronous callback


【解决方案1】:

我最近创建了一个名为 WaitFor 的简单抽象,用于在同步模式下调用异步函数(基于 Fibers):https://github.com/luciotato/waitfor

我没有用 async 包测试过它,但它应该可以工作。如果您遇到问题,请联系我。

使用 wait.for 和 async 你的代码将是:

var wait = require('waitfor');
var async = require('async');

function indexArticles(callback) {
  var files = wait.for(fs.readdir,"posts/");
  var markdown = wait.for(async.map, files, readPost);
  var results = wait.for(async.map, markdown, parse);
  var sorted = wait.for(async.sortBy, results, function(obj, callback) {
                                                  callback(null, obj.date);
                                              });
  callback( null, {"articles": sorted.reverse()} );
}

调用你的 fn(异步模式):

//execute in a fiber
wait.launchFiber(indexArticles,function(err,data){
       // do something with err,data
       }); 

调用你的 fn(同步模式):

//execute in a fiber
function handleRequest(req,res){
    try{
        ...
        data = wait.for(indexArticles); //call indexArticles and wait for results
        // do something with data
        res.end(data.toString());
    }
    catch(err){
        // handle errors
    }
}

// express framework
app.get('/posts', function(req, res) { 
    // handle request in a Fiber, keep node spinning
    wait.launchFiber(handleRequest,req,res);
    });

【讨论】:

    【解决方案2】:

    看起来我与 Brandon 的回答有些重叠,但这是我的看法:

     var async = require("async")
    
    //dummy function
    function passThrough(arg, callback){
      callback(null, arg)
    }
    
    //your code rewritten to only call the dummy. 
    //same structure, didn't want to think about files and markdown
    function indexArticles(callback) {
      passThrough("posts/", function(err, files) {
        async.map(files, passThrough, function(err, markdown) {
          async.map(markdown, passThrough, 
            function(err, results) {
              async.sortBy(results, function(obj, callback) {
                callback(err, obj);
            }, 
            function(err, sorted) {
              callback( {"articles": sorted.reverse()} );
            });
          });
        });
      });
    }
    indexArticles(console.log)
    
    //version of apply that calls 
    //fn(arg, arg, appliedArg, apliedArg, callback)
    function coolerApply(fn) {
      var args = Array.prototype.slice.call(arguments, 1);
      return function () {
        var callback = Array.prototype.slice.call(arguments, -1)
        var otherArgs = Array.prototype.slice.call(arguments, 0, -1)
        return fn.apply(
          null, otherArgs.concat(args).concat(callback)
        );
      };
    };
    
    //my version of your code that uses coolerAppl
    function indexArticles2(callback){
      async.waterfall([
        async.apply(passThrough, "posts/"),
        coolerApply(async.map, passThrough),
        coolerApply(async.map, passThrough),
        coolerApply(async.sortBy, function(obj, callback){callback(null,obj)})
      ],
      function(err, sorted){
        callback({"articles": sorted.reverse()})
      })
    }
    //does the same thing as indexArticles!
    indexArticles2(console.log)
    

    【讨论】:

      【解决方案3】:

      这是我目前为止的结果。

      function indexArticles(callback) {
        var flow = [
          async.apply(fs.readdir, "posts/"),
      
          function(data, callback) { async.map(data, readPost, callback); },
      
          function sortByDate(parsed, callback) {
            var iterator = function(obj, callback) {
              if (obj.date) { callback(null, obj.date); }
              else { callback("Article has no date.") }
            }
            // Note that this sorts in reverse lexicographical order!
            async.sortBy(parsed, iterator,
                function(err, sorted) { callback(err, {"articles": sorted.reverse()} ); }
              );
          }
        ];
      
        async.waterfall(flow, async.apply(callback))
      }
      

      【讨论】:

        【解决方案4】:

        这是一个有趣的问题,因为您需要将参数绑定到迭代器函数的左侧和右侧,因此 bind/ 和 bindRight(其中有一些 StackOverflow 上的实现)都不会为你工作。这里有几个选项供您选择:

        (1) 首先,在您的 async.waterfall 示例中,您有:

        function(callback) {
          fs.readdir("posts/", function(err, files) {
            callback(err, files)
          })
        }
        

        等同于:

        function(callback) {
          fs.readdir("posts/", callback)
        }
        

        使用Function.bind和这个方法,你的整个函数indexArticles可以写成:

        function indexArticles(callback) {
          async.waterfall([
            fs.readdir.bind(this, 'posts/'),
            function(files, cb) { async.map(files, readPost, cb); },
            function(text, cb) { async.map(text, parse, cb); },
            function(results, cb) { async.sortBy(results, function(obj, callback) {
              callback(null, obj.date);
            }, cb) }
          ], function(err, sorted) {
            callback( {"articles": sorted.reverse()} );
          });
        };
        

        这有点短。

        (2) 如果你真的想避免包装函数,你可以使用一种偏函数应用程序。首先,在文件顶部(或在模块等中),定义一个名为partial 的函数:

        var partial = function(fn) {
          var args = Array.prototype.slice.call(arguments, 1);
          return function() {
            var currentArg = 0;
            for(var i = 0; i < args.length && currentArg < arguments.length; i++) {
              if (args[i] === undefined)
                args[i] = arguments[currentArg++];
            }
            return fn.apply(this, args);
          };
        }
        

        此函数接受一个函数和任意数量的参数,并在调用函数时将参数列表中的undefined 值替换为实际参数。然后你会像这样使用它:

        function indexArticles(callback) {
          async.waterfall([
            fs.readdir.bind(this, 'posts/'),
            partial(async.map, undefined, readPost, undefined),
            partial(async.map, undefined, parse, undefined),
            partial(async.sortBy, undefined, function(obj, callback) {
              callback(null, obj.date);
            }, undefined)
          ], function(err, sorted) {
            callback( {"articles": sorted.reverse()} );
          });
        }
        

        因此,partial(async.map, undefined, readPost, undefined) 返回一个函数,当 Async 库以 fn(files, callback) 调用该函数时,它会为第一个 undefined 填充 files,为第二个 undefined 填充 callback,以致电async.map(files, readPost, callback)

        (3)Function.prototypethis StackOverflow answer还有一个partial的版本,允许你使用语法:async.map.partial(undefined, readPost, undefined);但是,我可能会建议不要以这种方式修改Function.prototype,而只使用partial 作为函数。

        最终,由您决定哪种方法最易读和可维护。

        【讨论】:

          猜你喜欢
          • 2018-06-27
          • 1970-01-01
          • 2017-11-21
          • 2017-09-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-03-20
          • 2017-04-08
          相关资源
          最近更新 更多