[英]How to avoid long nesting of asynchronous functions in 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();
});
});
});
如果有很多类似的功能,那么嵌套就会成为问题 。
有办法避免这种情况吗? 我想这与如何组合多个异步函数有关,这似乎是基本的东西。
有趣的观察。 请注意,在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在下面的注释中指出的那样,如果您要在内部函数中访问闭包变量,则以上内容将不是简单的转换。 在这种情况下,使用内联匿名函数更为可取。
凯,只需使用这些模块之一即可。
它将变成这样:
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()
}
);
您可以将此技巧用于数组而不是嵌套函数或模块。
在眼睛上容易得多。
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()();
在大多数情况下,我会同意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();
});
});
这种方法的优点是,您可以通过将“系列”功能更改为“并行”来快速更改代码以并行获取数据。 而且,async.js还将在浏览器中运行,因此,如果遇到任何棘手的异步代码,您可以使用与node.js中相同的方法。
希望这是有用的!
为此,我非常喜欢async.js 。
该问题通过瀑布命令解决:
瀑布(任务,[回调])
依次运行一个函数数组,每个函数将其结果传递给数组中的下一个函数。 但是,如果有任何函数将错误传递给回调,则不会执行下一个函数,并且会立即以错误调用主回调。
争论
任务-要运行的函数数组,每个函数都传递一个必须在完成时调用的回调(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'
});
至于req,res变量,它们将在与包围整个async.waterfall调用的function(req,res){}相同的范围内共享。
不仅如此,异步非常干净。 我的意思是,我改变了很多这样的情况:
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。 这大大加快了整个编码过程。
您需要的是一些语法糖。 看看这个:
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);
}
};
自从发现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();
});
});
我见过的最简单的语法糖是节点承诺。
npm安装节点承诺|| git clone https://github.com/kriszyp/node-promise
使用此方法,可以将异步方法链接为:
firstMethod().then(secondMethod).then(thirdMethod);
每个参数的返回值都可以用作下一个参数。
在此完成的工作采用了异步模式,并将其应用于依次调用的3个函数,每个函数在启动前都等待上一个函数完成-即,使它们同步 。 关于异步编程的要点是,您可以同时运行多个功能,而不必等待每个功能都完成。
如果getSomeDate()没有为getSomeOtherDate()提供任何内容,而没有为getMoreData()提供任何内容,那么为什么不按照js的要求异步调用它们,或者如果它们是相互依赖的(而不是异步的),则将它们写为单功能?
您无需使用嵌套来控制流程-例如,通过调用一个公共函数来确定每个函数的完成时间,该函数确定所有3个函数何时完成,然后发送响应。
在纯Javascript中使用闭包可以轻松避免回调地狱。 下面的解决方案假定所有回调都遵循function(error,data)签名。
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();
});
假设您可以这样做:
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();
}
我最近创建了一个名为wait.for的更简单的抽象,以在同步模式下(基于Fibre)调用异步函数。 它处于早期阶段,但可以正常工作。 它是在:
https://github.com/luciotato/waitfor
使用wait.for ,您可以调用任何标准的nodejs异步函数,就好像它是同步函数一样。
使用wait.for您的代码可以是:
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);
}
};
在所有情况下, getSomeDate , getSomeOtherDate和getMoreData应该是标准异步函数,最后一个参数是函数callback(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);
});
}
为了解决这个问题,我写了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();
});
显然,还有许多其他解决方案,但是预处理的优点是运行时开销极少或没有,而且得益于源映射的支持,调试也很容易。
我以一种非常原始但有效的方式来做。 例如,我需要与父母和孩子一起建立一个模型,假设我需要为他们做单独的查询:
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]);
}
使用Fibers 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"
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);
}
其他人回答后,您说您的问题是局部变量。 似乎很简单的方法是编写一个外部函数以包含那些局部变量,然后使用一堆命名内部函数并按名称访问它们。 这样,无论需要链接多少个函数,您都只会嵌套两个深度。
这是我的新手尝试通过嵌套使用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
也可以用作局部变量的持有者。 (这里,我有类似的参数sql
, bindings
和cb
,但是您可以在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为此很好地工作。 我碰到了这篇非常有用的文章,其中举例说明了async.js的需求和用法: http : //www.sebastianseilund.com/nodejs-async-in-practice
如果您不想使用“ step”或“ seq”,请尝试“ line”,这是减少嵌套异步回调的简单函数。
类似于C#的asyncawait是另一种方式
https://github.com/yortus/asyncawait
async(function(){
var foo = await(bar());
var foo2 = await(bar2());
var foo3 = await(bar2());
}
使用wire您的代码看起来像这样:
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();
});
});
就您所知,请考虑使用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), ]);
我有同样的问题。 我已经看到节点运行异步函数的主要库,它们呈现出非自然的链接(您需要使用三个或更多方法confs等)来构建代码。
我花了几周的时间来开发一种简单易读的解决方案。 请尝试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)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.