如何避免在Node.js中长时间嵌套异步函数

Kay*_*ale 156 javascript asynchronous functional-programming node.js

我想创建一个显示来自数据库的一些数据的页面,所以我创建了一些从我的数据库中获取数据的函数.我只是Node.js的新手,所以据我所知,如果我想在一个页面中使用所有这些(HTTP响应),我必须将它们全部嵌套:

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();
      });
    });
  });
Run Code Online (Sandbox Code Playgroud)

如果有很多这样的函数,那么嵌套就成了问题.

有办法避免这种情况吗?我想这与你如何组合多个异步函数有关,这似乎是一个基本的东西.

Dan*_*llo 73

有趣的观察.请注意,在JavaScript中,您通常可以使用命名函数变量替换内联匿名回调函数.

下列:

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 ...
});
Run Code Online (Sandbox Code Playgroud)

可以重写看起来像这样:

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);
Run Code Online (Sandbox Code Playgroud)

但是,除非您计划在其他位置重用回调逻辑,否则通常更容易读取内联匿名函数,如您的示例所示.它还可以让您不必为所有回调找到一个名称.

另外请注意,正如@pst在下面的注释中所指出的,如果您正在访问内部函数中的闭包变量,则上述内容不是简单的翻译.在这种情况下,使用内联匿名函数更为可取.

  • 但是,(并且实际上只是为了理解权衡)在非嵌套时,对变量*的某些闭包语义可能会丢失,因此它不是直接转换.在上面的例子中,`getMoreData`中对'res'的访问丢失了. (26认同)
  • 我认为你的解决方案已经破解:`someDataParser`实际上解析了所有数据,因为它也调用了`getMoreData`.从这个意义上说,函数名称是不正确的,很明显我们实际上没有删除嵌套问题. (2认同)

Bag*_*ggz 63

凯,只需使用其中一个模块.

它会变成这样:

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();
            });
        });
    });
});
Run Code Online (Sandbox Code Playgroud)

进入:

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()
    }
);
Run Code Online (Sandbox Code Playgroud)

  • 快速浏览一下flow-js,step和async,似乎它们只处理函数执行的顺序.在我的例子中,每个缩进都可以访问内联闭包变量.因此,例如,函数的工作方式如下:获取HTTP req/res,从DB获取用户ID以获取cookie,获取后续用户ID的电子邮件,获取更多后续电子邮件的数据,...,以后获取X Y,...如果我没有弄错,这些框架只能确保异步函数将以正确的顺序执行,但在每个函数体中都无法通过闭包获得自然提供的变量(?)谢谢:) (9认同)
  • 在对这些库进行排名方面,我在Github上检查了每个库中"星星"的数量.async最多,大约3000,Step接下来大约1000,其他大大减少.当然,他们并非都做同样的事情:-) (9认同)
  • @KayPale我倾向于使用async.waterfall,并且有时会为每个阶段/步骤提供我自己的函数,这些函数将传递下一步所需的内容,或者在async.METHOD调用之前定义变量,以便它可以在下线使用.同样会将METHODNAME.bind(...)用于我的异步.*调用,效果也很好. (3认同)

Cao*_*lan 18

在大多数情况下,我同意Daniel Vassallo.如果你可以将一个复杂且深度嵌套的函数分解为单独的命名函数,那么这通常是一个好主意.对于在单个函数中执行此操作的时间,您可以使用许多可用的node.js异步库之一.人们已经提出了许多不同的方法来解决这个问题,所以请看一下node.js模块页面,看看你的想法.

我自己写了一个模块,名为async.js.使用此,上面的示例可以更新为:

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();
  });
});
Run Code Online (Sandbox Code Playgroud)

这种方法的一个好处是,您可以通过将"系列"功能更改为"并行"来快速更改代码以并行获取数据.更重要的是,async.js也可以在浏览器中运行,因此如果您遇到任何棘手的异步代码,您可以使用与node.js相同的方法.

希望这很有用!

  • 您要实现的目标在架构上称为数据管道.对于这种情况,您可以使用异步瀑布. (3认同)

Gui*_*ido 18

您可以将此技巧用于数组而不是嵌套函数或模块.

眼睛容易得多.

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()();
Run Code Online (Sandbox Code Playgroud)

您可以扩展并行进程甚至并行进程链的惯用语:

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()();
Run Code Online (Sandbox Code Playgroud)


Gra*_* Li 15

为此,我非常喜欢async.js.

瀑布命令解决了这个问题:

瀑布(任务,[回调])

运行一系列函数,每个函数将结果传递给数组中的下一个.但是,如果任何函数将错误传递给回调,则不执行下一个函数,并立即调用主回调并显示错误.

参数

tasks - 要运行的函数数组,每个函数都传递一个必须在完成时调用的回调函数(err,result1,result2,...).第一个参数是一个错误(可以为null),任何进一步的参数将作为参数传递给下一个任务.callback(err,[results]) - 一旦完成所有函数,就会运行一个可选的回调函数.这将传递最后一个任务的回调结果.

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'    
});
Run Code Online (Sandbox Code Playgroud)

至于req,res变量,它们将在与函数(req,res){}相同的范围内共享,它包含整个async.waterfall调用.

不仅如此,异步非常干净.我的意思是我改变了很多这样的案例:

function(o,cb){
    function2(o,function(err, resp){
        cb(err,resp);
    })
}
Run Code Online (Sandbox Code Playgroud)

首先:

function(o,cb){
    function2(o,cb);
}
Run Code Online (Sandbox Code Playgroud)

然后到这个:

function2(o,cb);
Run Code Online (Sandbox Code Playgroud)

然后到这个:

async.waterfall([function2,function3,function4],optionalcb)
Run Code Online (Sandbox Code Playgroud)

它还允许从util.js非常快速地调用为async准备的许多预制函数.只需将您想要做的事情链接起来,确保o,cb得到普遍处理.这大大加快了整个编码过程.


gbl*_*zex 11

你需要的是一些语法糖.谢谢这个:

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();
}); 
Run Code Online (Sandbox Code Playgroud)

整洁,不是吗?你可能会注意到html变成了一个数组.这部分是因为字符串是不可变的,所以最好在数组中缓冲输出,而不是丢弃更大和更大的字符串.另一个原因是因为另一个很好的语法bind.

Queue在这个例子中,这只是一个例子,partial可以实现如下

// 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);
  }
};
Run Code Online (Sandbox Code Playgroud)


Sal*_*bas 7

自从我找到它之后就爱上了Async.js.它具有async.series可用于避免长嵌套的功能.

文档: -


系列(任务,[回调])

串行运行一系列函数,每个函数在前一个函数完成后运行.[...]

参数

tasks - 要运行的函数数组,每个函数都传递一个必须在完成时调用的回调函数. callback(err, [results]) - 所有功能完成后运行的可选回调.此函数获取传递给数组中使用的回调的所有参数的数组.


以下是我们如何将其应用于您的示例代码: -

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();
    });
});
Run Code Online (Sandbox Code Playgroud)


Nik*_*jan 6

我见过的最简单的语法糖是节点承诺.

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

使用此方法可以链接异步方法:

firstMethod().then(secondMethod).then(thirdMethod);
Run Code Online (Sandbox Code Playgroud)

每个的返回值在下一个中作为参数提供.