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

IT技术 javascript asynchronous functional-programming node.js
2021-01-11 21:31:46

我想制作一个页面来显示来自数据库的一些数据,所以我创建了一些从我的数据库获取数据的函数。我只是 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();
      });
    });
  });

如果有很多这样的函数,那么嵌套就会成为一个问题

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

6个回答

有趣的观察。请注意,在 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 ...
});

可以重写为如下所示:

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

但是,除非您计划在其他地方重用回调逻辑,否则阅读内联匿名函数通常要容易得多,如您的示例所示。它还将使您不必为所有回调查找名称。

另外请注意,正如@pst在下面的评论中指出的那样,如果您在内部函数中访问闭包变量,则上述内容将不是直接的翻译。在这种情况下,使用内联匿名函数更可取。

我认为你的解决方案坏了:someDataParser实际上解析所有数据,因为它也调用getMoreData. 从这个意义上说,函数名称是不正确的,很明显我们还没有真正消除嵌套问题。
2021-04-02 21:31:46
但是,(实际上只是为了理解权衡)当未嵌套时,变量上的一些闭包语义可能会丢失,因此它不是直接翻译。在上面的示例中,无法访问 'res' in getMoreData
2021-04-10 21:31:46

凯,只需使用这些module之一。

它会变成这样:

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

进入这个:

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()
    }
);
快速浏览一下 flow-js、step 和 async,它们似乎只处理函数执行的顺序。就我而言,每个缩进都可以访问内联闭包变量。例如,函数的工作方式如下:获取 HTTP req/res,从 DB 获取 cookie 的用户 ID,获取稍后用户 ID 的电子邮件,获取稍后电子邮件的更多数据,...,获取 X 获取稍后的 Y,...如果我没记错的话,这些框架只保证异步函数会以正确的顺序执行,但是在每个函数体中都没有办法获得闭包自然提供的变量(?)谢谢:)
2021-03-27 21:31:46
一个简单的问题:在您的module列表中,最后两个相同吗?即“async.js”和“async”
2021-03-28 21:31:46
@KayPale 我倾向于使用 async.waterfall,并且有时会为每个阶段/步骤提供自己的函数,这些函数将传递下一步需要的内容,或者在 async.METHOD 调用之前定义变量,以便它在下线可用。还将为我的 async.* 调用使用 METHODNAME.bind(...),这也很有效。
2021-04-06 21:31:46
在对这些库进行排名方面,我在 Github 上查看了每个库的“星星”数量。async 最多,大约有 3000 个,Step 其次是大约 1000 个,其他的明显更少。当然,他们并不都做同样的事情:-)
2021-04-07 21:31:46

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

我自己为此编写了一个module,称为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();
  });
});

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

希望有用!

嗨曹兰,谢谢你的回答!就我而言,每个缩进都可以访问内联闭包变量。例如,函数的工作方式如下:获取 HTTP req/res,从 DB 获取 cookie 的用户 ID,获取稍后用户 ID 的电子邮件,获取稍后电子邮件的更多数据,...,获取 X 获取稍后的 Y,...如果我没记错的话,您建议的代码只能确保异步函数以正确的顺序执行,但是在每个函数体中,都无法获得原始代码中闭包自然提供的变量。是这样吗?
2021-03-15 21:31:46
您试图实现的在架构上称为数据管道。您可以在这种情况下使用异步瀑布。
2021-03-30 21:31:46

您可以将此技巧用于数组而不是嵌套函数或module。

在眼睛上容易得多。

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

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

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

为此,我非常喜欢async.js

该问题通过瀑布命令解决:

瀑布(任务,[回调])

连续运行一系列函数,每个函数将其结果传递给数组中的下一个。但是,如果任何函数将错误传递给回调,则不会执行下一个函数,并且会立即调用带有错误的主回调。

参数

任务 - 要运行的函数数组,每个函数都传递一个回调(err,result1,result2,...),它必须在完成时调用。第一个参数是一个错误(可以为空),任何进一步的参数都将作为参数传递给下一个任务。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'    
});

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

不仅如此,async 非常干净。我的意思是我改变了很多这样的情况:

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

首先:

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

然后到这个:

function2(o,cb);

然后到这个:

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

它还允许从 util.js 非常快地调用许多为异步准备的预制函数。只要把你想做的事情串联起来,确保 o,cb 被普遍处理。这大大加快了整个编码过程。