[英]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.