问题
A couple years ago I was experimenting with NodeJS and found that the "Step" library cleaned up some of my code rather nicely. When desiring to bring that code up to date, I noticed a few "red flags" on Step. (Not being updated for a couple of years, just 32 commits, etc.)
So I looked around and found Async.js, which has more features and active maintenance.
Looks nice and general. But I started trying to apply a transformation to use it instead, and might not be taking the right angle.
If I'm reading correctly, the core function of Step seems to be what Async.JS would call the "waterfall" pattern. So in Step you would write:
Step(
function firstStepNoArgs() {
foo.asyncCall(this);
},
function secondStep(err, argFromFoo) {
if (err) {
handleError(err);
}
bar.asyncCall(argFromFoo, 1, this.parallel());
baz.asyncCall(argFromFoo, 2, this.parallel());
},
function thirdStep(err, argFromBar, argFromBaz) {
if (err) {
handleError(err);
}
/* etc... */
}
);
If I didn't know any better, I might guess you'd do that in async.js like this (untested, consider it pseudocode; I'm talking about a theoretical change I haven't actually pursued yet)
function thirdStep(argFromBar, argFromBaz) {
/* etc... */
}
async.waterfall([
function firstStepNoArgs(callback) {
foo.asyncCall(callback);
},
function secondStep(argFromFoo, callback) {
async.parallel([
barResult: function(callback) {
bar.asyncCall(parameterFromFoo, 1, callback);
},
bazResult: function(callback) {
baz.asyncCall(parameterFromFoo, 2, callback);
}
],
function(err, result) {
if (err) {
handleError(err);
} else {
thirdStep(result.barResult, result.bazResult);
}
}
}
],
function(err, result) {
if (err) {
handleError(err);
} else {
/* no-op? just assume third-step runs? */
}
}
);
Step was very focused and sequential, and my little draft here shows it getting messy in the adaptation. Am I missing something?
So my question is: What is the right way to convert the clear Step code into Async.JS? Or have I chosen the wrong library to upgrade to? I don't want my code to get uglier, but I don't want to depend on a library that seems kind of "dead", either. :-/
回答1:
As requested, an implementation of what you are doing with promises. Just paste and run and you should get the idea. Bear in mind the top half of the code is setting up simulated functions so you can better follow along how this works. Someone will probably tell me I should have done this as a gist, which I may do as well.
var Q = require('q');
var foo ={}, bar ={}, baz = {};
// let's mock up some of your objects with some asynch functions
// using setTimeout for async completion
foo.asyncCall = function ( cb) {
setTimeout(function(){ cb(null, 'promises'); },500);
};
bar.asyncCall = function ( arg1, arg2, cb) {
setTimeout(function(){
var result = arg1 + ' can be ' + arg2;
cb(null, result);
},1200);
};
// going to add a will-always-fail function for example purposes
bar.asyncFailure = function (arg1, arg2, cb){
setTimeout(function(){
cb(new Error(arg1 +' offer decent error handling'), null);
},2000); // longer delay - simulate a timeout maybe
};
baz.asyncCall = function ( arg1, arg2, cb) {
setTimeout(function(){
var result = arg1 + ' are really ' + arg2;
cb(null, result);
},800);
};
// set up promise-enbaled calls. Q.denodeify is an easy way to deal with any
// standard node function with a final parameter being an (err,data) callback
// If these are your own functions, you can also create your own promises, but
// Q.nodeify is probably the fastest way to adapt existing code.
bar.promiseFailure = Q.denodeify(bar.asyncFailure);
bar.promiseCall = Q.denodeify(bar.asyncCall);
baz.promiseCall = Q.denodeify(baz.asyncCall);
// this is your wrap up call ('thirdStep' in your code)
function allTogetherNow(arg1, arg2) {
console.log(arg1 +'\n' + arg2);
};
// now we can have some fun
// an example that will run to completion normally
// Q.ninvoke is sort of a 'one-time' denodeify, it invokes a node-style function
// and returns a promise
function example(){
Q.ninvoke(foo,'asyncCall')
.then( function (x) {
return [bar.promiseCall(x, 'confusing at first'),
baz.promiseCall(x, 'awesome after that')]
})
.spread(allTogetherNow)
.fail(function(e){console.log('Had an error')})
.finally(function(){console.log('Calling no matter what from example')});
};
// sometimes things aren't entirely fun though, and there can be an error
function example2(){
Q.ninvoke(foo,'asyncCall')
.then( function (x) {
return [bar.promiseFailure(x, 'confusing at first'),
baz.promiseCall(x, 'awesome after that')]
})
.spread(allTogetherNow)
.fail(function(e){console.log(e)})
.finally(function(){console.log('Calling no matter what from example2')});
};
example();
example2();
For those that don't want to bother running it, the output emitted is:
promises can be confusing at first
promises are really awesome after that
Calling no matter what from example
[Error: promises offer decent error handling]
Calling no matter what from example2
回答2:
Be careful with your callbacks, here. The callback signature is:
callback(err, arg1, arg2 ...)
So if your foo.asyncCall
were to call it with:
callback(result1, result2)
Then the whole async would mysteriously fail from that point. The correct callback for success should start with a null, e.g.
callback(null, result1, result2)
Here is the corrected code:
function thirdStep(argFromBar, argFromBaz, callback) {
/* etc... */
callback(null, result);
}
async.waterfall([
function firstStepNoArgs(callback) {
// error callback("failed"). First args is not null means failed
// in case of error, it just goes straight to function(err, result)
foo.asyncCall(callback);
},
function secondStep(argFromFoo, callback) {
// argFromFoo come from previous callback (in this case, result1)
async.parallel([
barResult: function(callback) {
bar.asyncCall(parameterFromFoo, 1, callback);
},
bazResult: function(callback) {
baz.asyncCall(parameterFromFoo, 2, callback);
}
],
function(err, result) {
if (err) {
// in case of error you should do callback(error),
// this callback is from secondStep(argFromFoo, callback).
// this will pass to final function(err, result).
handleError(err);
} else {
// you need to do callback(null) inside thirdStep
// if callback is not called, the waterfall won't complete
thirdStep(result.barResult, result.bazResult, callback);
}
}
}
],
function(err, result) {
if (err) {
handleError(err);
} else {
// everything is executed correctly,
// if any step failed it will gone to err.
}
}
);
来源:https://stackoverflow.com/questions/22138759/how-to-convert-step-code-to-async-js-step-waterfall-this-parallel