原文地址: https://medium.com/@rdsubhas/es6-from-callbacks-to-promises-to-generators-87f1c0cd8f2e#.ob6888mul
Let’s take a real life use case with the request library.
This is how typical NodeJS code would look like. Every function receives a callback function with a commonly used signature: function(err, response) { }
var request = require('request');
var url1='http://httpbin.org/', url2=url1, url3=url1, url4=url1;
function foo(finalCallback) {
request.get(url1, function(err1, res1) {
if (err1) { return finalCallback(err1); }
request.post(url2, function(err2, res2) {
if (err2) { return finalCallback(err2); }
request.put(url3, function(err3, res3) {
if (err3) { return finalCallback(err3); }
request.del(url4, function(err4, res4) {
// let's stop here
if (err4) { return finalCallback(err4); }
finalCallback(null, "whew all done");
})
})
})
})
}
// use that function somewhere
Promise libraries take the typical callback function: function(err, response) { } And split those arguments into separate then/catch chainable callbacks: .then(function(response) { }).catch(function(err) { })
You can use Q, Bluebird or any of the innumerable promise libraries. I’ve used bluebird here. You have to first “promisify” the old-style callback library methods.
Note how you have to use “request.getAsync” instead of “request.get”. That’s called “promisification” (line #2) — it converts regular methods into promise-style methods. Also note how it simplifies the place where foo() is invoked as well.
Let’s combine the power of promises and ES6 generators.
“foo” is now almost sequential. We reduced the program from 27 lines (callback style) to 23 lines (promises) to 19 lines (promises + generators). It now appears completely flat without any nested functions. Of course, behind the scenes there are still callbacks happening, but WYSIWYG.
var Promise = require('bluebird');
var request = Promise.promisifyAll(require('request'));
var url1='http://httpbin.org/', url2=url1, url3=url1, url4=url1;
function foo() {
return request.getAsync(url1)
.then(function(res1) {
return request.postAsync(url2);
}).then(function(res2) {
return request.putAsync(url3);
}).then(function(res3) {
return request.delAsync(url4);
}).then(function(res4) {
return "whew all done";
});
}
// use that function somewhere
foo().then(function(message) {
console.log("success!", message);
}).catch(function(err) {
console.log("error!", err);
});
Note that the place where we call “foo” still uses promises. We can flatten that as well to use generators, and it becomes a simple try/catch.
function* callerFunction() {
try {
message = yield foo();
console.log("success!", message);
} catch (err) {
console.log("error!", err);
}
}
callerFunction = Promise.coroutine(callerFunction);
callerFunction();
And the place where you call “callerFunction” can also be flattened, and so on, and so on until the topmost entry point of the app. For web applications, that entry point is the web framework. If the web framework is aware of using generators and promises, you can basically make all your functions as generators and forever flat. And you get close to something like koa.
Generators are immensely useful in writing Tests (Mocha/etc).Tests usually have a lot of callbacks, but they run in sequence. What a waste of asynchronicity. You can write test cases today using Generators, without worrying about switching testing frameworks.
var Promise = require('bluebird');
var assert = require('assert');
var request = Promise.promisifyAll(require('request'));
var url1='http://httpbin.org/', url2=url1, url3=url1, url4=url1;
describe('context', function() {
it('should kill callbacks', Promise.coroutine(function*() {
// Use destructuring since "request" returns multiple values
[res1] = yield request.getAsync(url1);
assert.equal(200, res1.statusCode);
// Array destructuring might not work in some node versions
// In that case, simply use res[0].statusCode
[res2] = yield request.getAsync(url2);
assert.equal(200, res2.statusCode);
[res3] = yield request.getAsync(url3);
assert.equal(200, res3.statusCode);
[res4] = yield request.getAsync(url4);
assert.equal(200, res4.statusCode);
// Yay! No more "done()" and no more callbacks
}));
});
ES7 async/await further works on top of generators. Babel already has it in enabled (though its still in staging), so you can try this today.
var Promise = require('bluebird');
var request = Promise.promisifyAll(require('request'));
var url1='http://httpbin.org/', url2=url1, url3=url1, url4=url1;
async function foo() {
var res1 = await request.getAsync(url1);
var res2 = await request.getAsync(url2);
var res3 = await request.getAsync(url3);
var res4 = await request.getAsync(url4);
return "whew all done";
}
// use that function somewhere
foo().then(function(message) {
console.log("success!", message);
}).catch(function(err) {
console.log("error!", err);
});
This whole magic simply works because NodeJS callbacks have a standard signature of function(err, response) { }.
Promise libraries simply act as a glue between your code, target function (request.get/post/…) and a Deferred object.
deferred = // create a Deferred() object
customCallback = function(err, response) {
if (err) deferred.reject(err);
else deferred.resolve(response);
}
// call original request.get/post/... with customCallback
// return you the Deferred's promise
When ES6 introduced generators, Promise libraries hooked in with a nify hack: If you “yield a promise”, you’ll get the resolved value back.
try {
response = yield request.getAsync(...)
^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
to get the response yield a promise
} catch (error) { }
^^^^^^^^^^^^^^^^^^^^^
catch the error
How Generators work is a larger topic altogether. I recommend reading
davidwalsh.name/es6-generators or any of the innumerable articles on the web.