重构嵌套回调、node.js、异步
Posted
技术标签:
【中文标题】重构嵌套回调、node.js、异步【英文标题】:Refactoring nested callbacks, node.js, async 【发布时间】:2012-07-15 21:42:11 【问题描述】:function indexArticles(callback)
fs.readdir("posts/", function(err, files)
async.map(files, readPost, function(err, markdown)
async.map(markdown, parse, function(err, results)
async.sortBy(results, function(obj, callback)
callback(err, obj.date);
, function(err, sorted)
callback( "articles": sorted.reverse() );
);
);
);
);
我正试图弄清楚如何让这个更漂亮——正如你所知道的,我正在使用 caolan 的异步库,但我不确定要使用哪个控制流结构。例如,如果我使用 async.waterfall 似乎会产生更多代码,每个步骤都必须包装在一个匿名函数中。例如,这只是带有瀑布的嵌套版本的前两行:
function indexArticles(callback)
async.waterfall([
function(callback)
fs.readdir("posts/", function(err, files)
callback(err, files)
)
,
function(files, callback)
async.map(files, readPost, function(err, markdown)
callback(err, markdown)
)
])
您将如何改进?
如果有一种方法不仅可以从左侧部分应用参数,那么我可以看到这样做,例如,
function indexArticles(callback)
async.waterfall([
async.apply(fs.readdir, "posts/"),
async.apply(async.map, __, readPost),
async.apply(async.map, __, parse),
// etc...
])
【问题讨论】:
虽然waterfall
最终可能会包含更多字符,但我认为它最终会更具可读性。另请查看apply
以帮助处理所有这些匿名函数。
你能看一下我刚刚发布的瀑布示例并告诉我我做得对吗?
【参考方案1】:
我最近创建了一个名为 WaitFor 的简单抽象,用于在同步模式下调用异步函数(基于 Fibers):https://github.com/luciotato/waitfor
我没有用 async 包测试过它,但它应该可以工作。如果您遇到问题,请联系我。
使用 wait.for 和 async 你的代码将是:
var wait = require('waitfor');
var async = require('async');
function indexArticles(callback)
var files = wait.for(fs.readdir,"posts/");
var markdown = wait.for(async.map, files, readPost);
var results = wait.for(async.map, markdown, parse);
var sorted = wait.for(async.sortBy, results, function(obj, callback)
callback(null, obj.date);
);
callback( null, "articles": sorted.reverse() );
调用你的 fn(异步模式):
//execute in a fiber
wait.launchFiber(indexArticles,function(err,data)
// do something with err,data
);
调用你的 fn(同步模式):
//execute in a fiber
function handleRequest(req,res)
try
...
data = wait.for(indexArticles); //call indexArticles and wait for results
// do something with data
res.end(data.toString());
catch(err)
// handle errors
// express framework
app.get('/posts', function(req, res)
// handle request in a Fiber, keep node spinning
wait.launchFiber(handleRequest,req,res);
);
【讨论】:
【参考方案2】:看起来我与 Brandon 的回答有些重叠,但这是我的看法:
var async = require("async")
//dummy function
function passThrough(arg, callback)
callback(null, arg)
//your code rewritten to only call the dummy.
//same structure, didn't want to think about files and markdown
function indexArticles(callback)
passThrough("posts/", function(err, files)
async.map(files, passThrough, function(err, markdown)
async.map(markdown, passThrough,
function(err, results)
async.sortBy(results, function(obj, callback)
callback(err, obj);
,
function(err, sorted)
callback( "articles": sorted.reverse() );
);
);
);
);
indexArticles(console.log)
//version of apply that calls
//fn(arg, arg, appliedArg, apliedArg, callback)
function coolerApply(fn)
var args = Array.prototype.slice.call(arguments, 1);
return function ()
var callback = Array.prototype.slice.call(arguments, -1)
var otherArgs = Array.prototype.slice.call(arguments, 0, -1)
return fn.apply(
null, otherArgs.concat(args).concat(callback)
);
;
;
//my version of your code that uses coolerAppl
function indexArticles2(callback)
async.waterfall([
async.apply(passThrough, "posts/"),
coolerApply(async.map, passThrough),
coolerApply(async.map, passThrough),
coolerApply(async.sortBy, function(obj, callback)callback(null,obj))
],
function(err, sorted)
callback("articles": sorted.reverse())
)
//does the same thing as indexArticles!
indexArticles2(console.log)
【讨论】:
【参考方案3】:这是我目前为止的结果。
function indexArticles(callback)
var flow = [
async.apply(fs.readdir, "posts/"),
function(data, callback) async.map(data, readPost, callback); ,
function sortByDate(parsed, callback)
var iterator = function(obj, callback)
if (obj.date) callback(null, obj.date);
else callback("Article has no date.")
// Note that this sorts in reverse lexicographical order!
async.sortBy(parsed, iterator,
function(err, sorted) callback(err, "articles": sorted.reverse() );
);
];
async.waterfall(flow, async.apply(callback))
【讨论】:
【参考方案4】:这是一个有趣的问题,因为您需要将参数绑定到迭代器函数的左侧和右侧,因此 bind
/ 和 bindRight
(其中有一些 *** 上的实现)都不会为你工作。这里有几个选项供您选择:
(1) 首先,在您的 async.waterfall
示例中,您有:
function(callback)
fs.readdir("posts/", function(err, files)
callback(err, files)
)
等同于:
function(callback)
fs.readdir("posts/", callback)
使用Function.bind
和这个方法,你的整个函数indexArticles
可以写成:
function indexArticles(callback)
async.waterfall([
fs.readdir.bind(this, 'posts/'),
function(files, cb) async.map(files, readPost, cb); ,
function(text, cb) async.map(text, parse, cb); ,
function(results, cb) async.sortBy(results, function(obj, callback)
callback(null, obj.date);
, cb)
], function(err, sorted)
callback( "articles": sorted.reverse() );
);
;
这有点短。
(2) 如果你真的想避免包装函数,你可以使用一种偏函数应用程序。首先,在文件顶部(或在模块等中),定义一个名为partial
的函数:
var partial = function(fn)
var args = Array.prototype.slice.call(arguments, 1);
return function()
var currentArg = 0;
for(var i = 0; i < args.length && currentArg < arguments.length; i++)
if (args[i] === undefined)
args[i] = arguments[currentArg++];
return fn.apply(this, args);
;
此函数接受一个函数和任意数量的参数,并在调用函数时将参数列表中的undefined
值替换为实际参数。然后你会像这样使用它:
function indexArticles(callback)
async.waterfall([
fs.readdir.bind(this, 'posts/'),
partial(async.map, undefined, readPost, undefined),
partial(async.map, undefined, parse, undefined),
partial(async.sortBy, undefined, function(obj, callback)
callback(null, obj.date);
, undefined)
], function(err, sorted)
callback( "articles": sorted.reverse() );
);
因此,partial(async.map, undefined, readPost, undefined)
返回一个函数,当 Async 库以 fn(files, callback)
调用该函数时,它会为第一个 undefined
填充 files
,为第二个 undefined
填充 callback
,以致电async.map(files, readPost, callback)
。
(3)Function.prototype
this *** answer还有一个partial
的版本,允许你使用语法:async.map.partial(undefined, readPost, undefined)
;但是,我可能会建议不要以这种方式修改Function.prototype
,而只使用partial
作为函数。
最终,由您决定哪种方法最易读和可维护。
【讨论】:
以上是关于重构嵌套回调、node.js、异步的主要内容,如果未能解决你的问题,请参考以下文章