如何避免在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();
      });
    });
  });

如果有很多类似的功能,那么嵌套就会成为问题

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

老丝阿飞2020/03/24 18:05:52

类似于C#的asyncawait是执行此操作的另一种方法

https://github.com/yortus/asyncawait

async(function(){

    var foo = await(bar());
    var foo2 = await(bar2());
    var foo3 = await(bar2());

}
路易GO2020/03/24 18:05:52

如果您不想使用“ step”或“ seq”,请尝试“ line”,这是减少嵌套异步回调的简单函数。

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

斯丁前端2020/03/24 18:05:52

async.js为此很好地工作。我碰到了这篇非常有用的文章,其中举例说明了async.js的需求和用法:http : //www.sebastianseilund.com/nodejs-async-in-practice

伽罗理查德2020/03/24 18:05:52

其他人回答后,您说您的问题是局部变量。似乎很简单的方法是编写一个外部函数以包含那些局部变量,然后使用一堆命名内部函数并按名称访问它们。这样,无论您需要链接多少个函数,您都只会嵌套两个深度。

这是我的新手尝试将mysqlNode.js模块与嵌套一起使用的尝试

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

以下是使用命名内部函数的重写。外部函数with_connection也可以用作局部变量的保持器。(在这里,我已经得到了参数sqlbindingscb以类似的方式行为,但你可以在定义一些额外的局部变量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);
}

我一直在想,也许可以用一个实例变量来创建一个对象,并用这些实例变量代替局部变量。但是现在我发现,使用嵌套函数和局部变量的上述方法更简单易懂。取消学习OO需要花费一些时间,看来:-)

所以这是我先前的版本,其中包含对象和实例变量。

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

事实证明,bind可以利用它来获得一些优势。它使我摆脱了我创建的有些丑陋的匿名函数,除了将自身转发给方法调用之外,它们没有做任何事情。我无法直接传递该方法,因为它会涉及到错误的值this但是,使用bind,我可以指定所需的值this

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

当然,这些都不是使用Node.js编码的正确JS-我只花了几个小时就可以了。但是,也许稍微抛光一下,这项技术就能有所帮助吗?

DavaidTony宝儿2020/03/24 18:05:52

Task.js为您提供:

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

代替这个:

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);
}
蛋蛋猿2020/03/24 18:05:52

我以一种非常原始但有效的方式来做。例如,我需要与父母和孩子一起建立一个模型,假设我需要为他们做单独的查询:

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]);
}
Mandy猿理查德2020/03/24 18:05:51

您在此处所做的事情是采用异步模式,并将其应用于依次调用的3个函数,每个函数在启动前都等待上一个函数完成-即,使它们同步异步编程的要点是,您可以同时运行多个功能,而不必等待每个功能都完成。

如果getSomeDate()没有为getSomeOtherDate()提供任何内容,而没有为getMoreData()提供任何内容,那么为什么不按照js的要求异步调用它们,或者如果它们是相互依赖的(而不是异步的),则将它们写为单功能?

您无需使用嵌套来控制流程-例如,通过调用一个确定所有3个控件何时完成并发送响应的通用函数来使每个函数完成。

小小2020/03/24 18:05:51

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

npm安装节点承诺|| git clone https://github.com/kriszyp/node-promise

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

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

每个参数的返回值都可以用作下一个参数。

神奇伽罗Eva2020/03/24 18:05:51

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

在眼睛上容易得多。

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()();
伽罗2020/03/24 18:05:51

有趣的观察。请注意,在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在下面的注释中指出的那样,如果您在内部函数中访问闭包变量,则上述内容将不是简单的转换。在这种情况下,使用内联匿名函数更为可取。