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

如果有许多这样的函数,那么 筑巢成了一个问题

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

63341 次浏览

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

你需要的是一点句法糖,看看这个:

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

漂亮的 整洁,不是吗?您可能注意到 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);
}
};

你所做的就是将一个异步模式应用到3个按顺序调用的函数中,每个函数都要等到前一个函数完成后才能启动——也就是说,你把它们设置为 同步。关于异步编程的要点是,您可以让多个函数同时运行,而不必等待每个函数完成。

如果 getSome Date ()没有提供任何东西给 getSome OtherDate () ,而它也没有提供任何东西给 getMoreData () ,那么为什么不像 js 允许的那样异步调用它们,或者如果它们是相互依赖的(而不是异步的) ,那么将它们写成一个单独的函数呢?

您不需要使用嵌套来控制流——例如,通过调用一个公共函数来完成每个函数,该函数确定所有3个函数什么时候完成,然后发送响应。

大多数情况下,我同意 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();
});
});

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

希望有用!

假设你可以这样做:

http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
var html = "<h1>Demo page</h1>";
chain([
function (next) {
getSomeDate(client, next);
},
function (next, someData) {
html += "<p>"+ someData +"</p>";
getSomeOtherDate(client, next);
},
function (next, someOtherData) {
html += "<p>"+ someOtherData +"</p>";
getMoreData(client, next);
},
function (next, moreData) {
html += "<p>"+ moreData +"</p>";
res.write(html);
res.end();
}
]);
});

您只需要实现 chain () ,这样它就可以部分地将每个函数应用于下一个函数,并立即仅调用第一个函数:

function chain(fs) {
var f = function () {};
for (var i = fs.length - 1; i >= 0; i--) {
f = fs[i].partial(f);
}
f();
}

凯,只要使用这些模块之一。

它会把这个变成:

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

我也有同样的问题。我见过主要的节点库运行异步函数,它们表现出非自然的链接(您需要使用三个或更多的方法 conf 等)来构建代码。

我花了几个星期的时间想出了一个简单易读的解决方案。请试试 EnqJS。所有的意见我们都会感激的。

而不是:

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

与 EnqJS:

http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
var html = "<h1>Demo page</h1>";


enq(function(){
var self=this;
getSomeDate(client, function(someData){
html += "<p>"+ someData +"</p>";
self.return();
})
})(function(){
var self=this;
getSomeOtherDate(client, function(someOtherData){
html += "<p>"+ someOtherData +"</p>";
self.return();
})
})(function(){
var self=this;
getMoreData(client, function(moreData) {
html += "<p>"+ moreData +"</p>";
self.return();
res.write(html);
res.end();
});
});
});

注意,代码看起来比以前更大了,但是没有像以前那样嵌套。 为了表现得更自然,这些链条立即被称为:

enq(fn1)(fn2)(fn3)(fn4)(fn4)(...)

要说它返回了,在我们调用的函数中:

this.return(response)

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

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

我爱上了 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();
});
});

使用纤维 https://github.com/laverdet/node-fibers它使异步代码看起来像同步的(没有阻塞)

我个人使用这个小小的包装 http://alexeypetrushin.github.com/synchronize 我的项目中的代码示例(每个方法实际上都是异步的,使用的是异步文件 IO) ,我甚至害怕想象如果使用回调或异步控制流辅助程序库会有多么混乱。

_update: (version, changesBasePath, changes, oldSite) ->
@log 'updating...'
@_updateIndex version, changes
@_updateFiles version, changesBasePath, changes
@_updateFilesIndexes version, changes
configChanged = @_updateConfig version, changes
@_updateModules version, changes, oldSite, configChanged
@_saveIndex version
@log "updated to #{version} version"

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

看起来容易多了。

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

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

我见过的最简单的语法 Sugar 就是 Node- 承诺。

Npm 安装 node-諾 | | git 克隆 https://github.com/kriszyp/node-promise

使用这种方法,您可以将异步方法链接为:

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

每个参数的返回值在下一个。

我最近创建了一个更简单的抽象 等等,用于在同步模式下(基于纤程)调用异步函数。虽然还处于早期阶段,但已经奏效了。现在是:

Https://github.com/luciotato/waitfor

使用 等等,您可以调用任何标准 nodejs 异步函数,就好像它是一个 sync 函数一样。

使用 等等你的代码可以是:

var http=require('http');
var wait=require('wait.for');


http.createServer(function(req, res) {
wait.launchFiber(handleRequest,req, res); //run in a Fiber, keep node spinning
}).listen(8080);




//in a fiber
function handleRequest(req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
var html = "<h1>Demo page</h1>";
var someData = wait.for(getSomeDate,client);
html += "<p>"+ someData +"</p>";
var someOtherData = wait.for(getSomeOtherDate,client);
html += "<p>"+ someOtherData +"</p>";
var moreData = wait.for(getMoreData,client);
html += "<p>"+ moreData +"</p>";
res.write(html);
res.end();
};

... 或者如果您希望减少冗长(还要添加错误捕捉)

//in a fiber
function handleRequest(req, res) {
try {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write(
"<h1>Demo page</h1>"
+ "<p>"+ wait.for(getSomeDate,client) +"</p>"
+ "<p>"+ wait.for(getSomeOtherDate,client) +"</p>"
+ "<p>"+ wait.for(getMoreData,client) +"</p>"
);
res.end();
}
catch(err) {
res.end('error '+e.message);
}


};

在所有情况下,获得某人的约会获取其他约会和 < em > getMoreData 应该是带有最后一个参数 a < em > 函数回调(err,data)的标准异步函数

例如:

function getMoreData(client, callback){
db.execute('select moredata from thedata where client_id=?',[client.id],
,function(err,data){
if (err) callback(err);
callback (null,data);
});
}

在其他人回应后,你说你的问题是局部变量。看起来一个简单的方法是编写一个外部函数来包含这些本地变量,然后使用一组命名的内部函数并通过名称访问它们。这样,不管需要链接多少个函数,您都只需要嵌套两个深度。

下面是我的新手在嵌套时使用 mysql Node.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 ——我只是花了几个小时在上面。但是也许稍微润色一下这项技术就能有所帮助?

为此,我非常喜欢 Async.js

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

瀑布(任务,[回调])

以串联方式运行函数数组,每个函数将其结果传递给数组中的下一个函数。但是,如果任何函数将错误传递给回调函数,则不执行下一个函数,并立即使用错误调用主回调函数。

争论

Task ——要运行的函数数组,每个函数都会被传递一个回调函数(err、 result t1、 result t2、 ...) ,它必须在完成时调用。第一个参数是一个错误(可以为 null) ,任何进一步的参数都将作为参数传递给下一个任务。 Callback (err,[ result ])——所有函数完成后运行的可选回调。这将传递最后一个任务的回调结果。

例子

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 变量,它们将在与函数(req,res){}相同的范围内共享,函数(req,res){}包含了整个 sync.waterfall 调用。

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

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 调用为异步准备的许多预制函数。只要把你想做的事情串起来,确保所有人都能处理好 cb。这大大加快了整个编码过程。

为了解决这个问题,我编写了 nodent (https://npmjs.org/package/nodent) ,它可以对 JS 进行不可见的预处理。您的示例代码将变成(异步的,真正的-阅读文档)。

http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
var html = "<h1>Demo page</h1>";
someData <<= getSomeDate(client) ;


html += "<p>"+ someData +"</p>";
someOtherData <<= getSomeOtherDate(client) ;


html += "<p>"+ someOtherData +"</p>";
moreData <<= getMoreData(client) ;


html += "<p>"+ moreData +"</p>";
res.write(html);
res.end();
});

显然,还有许多其他的解决方案,但是预处理的优势在于运行时开销很少或根本没有开销,而且由于源地图支持,调试也很容易。

Js 可以很好地解决这个问题。我偶然发现了这篇非常有用的文章,它通过例子解释了使用和需要 sync.js 的方法: http://www.sebastianseilund.com/nodejs-async-in-practice

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

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

类似 C # 的异步等待是实现这一点的另一种方法

Https://github.com/yortus/asyncawait

async(function(){


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


}

使用 电线您的代码将如下所示:

http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});


var l = new Wire();


getSomeDate(client, l.branch('someData'));
getSomeOtherDate(client, l.branch('someOtherData'));
getMoreData(client, l.branch('moreData'));


l.success(function(r) {
res.write("<h1>Demo page</h1>"+
"<p>"+ r['someData'] +"</p>"+
"<p>"+ r['someOtherData'] +"</p>"+
"<p>"+ r['moreData'] +"</p>");
res.end();
});
});

回调地狱可以很容易地避免在纯 javascript 与闭包。下面的解决方案假定所有回调都遵循函数(错误、数据)签名。

http.createServer(function (req, res) {
var modeNext, onNext;


// closure variable to keep track of next-callback-state
modeNext = 0;


// next-callback-handler
onNext = function (error, data) {
if (error) {
modeNext = Infinity;
} else {
modeNext += 1;
}
switch (modeNext) {


case 0:
res.writeHead(200, {'Content-Type': 'text/html'});
var html = "<h1>Demo page</h1>";
getSomeDate(client, onNext);
break;


// handle someData
case 1:
html += "<p>"+ data +"</p>";
getSomeOtherDate(client, onNext);
break;


// handle someOtherData
case 2:
html += "<p>"+ data +"</p>";
getMoreData(client, onNext);
break;


// handle moreData
case 3:
html += "<p>"+ data +"</p>";
res.write(html);
res.end();
break;


// general catch-all error-handler
default:
res.statusCode = 500;
res.end(error.message + '\n' + error.stack);
}
};
onNext();
});

你可以考虑一下 Jazz.js Https://github.com/javanile/jazz.js/wiki/script-showcase



const jj = require('jazz.js');


// ultra-compat stack
jj.script([
a => ProcessTaskOneCallbackAtEnd(a),
b => ProcessTaskTwoCallbackAtEnd(b),
c => ProcessTaskThreeCallbackAtEnd(c),
d => ProcessTaskFourCallbackAtEnd(d),
e => ProcessTaskFiveCallbackAtEnd(e),
]);