What is the benefit of a 'promise' abstraction in CommonJS?
I'm reading this article and the section on the promise abstraction seems a little overly complicated to me. The following is given as an example:
requestSomeData("http://example.com/foo") // returns a promise for the response
.then(function(response){ // ‘then’ is used to provide a promise handler
return JSON.parse(response.body); // parse the body
}) // returns a promise for the parsed body
.then(function(data){
return data.price; // get the price
}) // returns a promise for the price
.then(function(price){ // print out the price when it is fulfilled
print("The price is " + price);
});
It seems to me that the following could provide the same result with fewer lines of code:
requestSomeData("http://example.com/foo")
.requestHandler(function(response){
// parse the body
var d开发者_高级运维ata = JSON.parse(response.body);
// get the price
var price = data.price;
// print out the price
print("The price is " + price);
});
While it is true that both will ultimately accomplish the same thing, the difference is that your second example is not asynchronous. For example, consider what happens if JSON.parse(...)
turns out to be an extremely expensive operation; you'll have to hang until everything's finished, which may not always be what you want.
That's what promises get you: the powerful ability to defer the computation of the right answer until a more convenient time. As the name suggests, the construct "promises" to give you the result at some point, just not necessarily right now. You can read more about futures and promises work on a larger scale here.
Let's compare the promise example to a pure Javascript example:
// First we need a convenience function for W3C's fiddly XMLHttpRequest.
// It works a little differently from the promise framework. Instead of
// returning a promise to which we can attach a handler later with .then(),
// the function accepts the handler function as an argument named 'callback'.
function requestSomeDataAndCall(url, callback) {
var req = new XMLHttpRequest();
req.onreadystatechange = resHandler;
req.open("GET", url, false);
req.send();
function resHandler() {
if (this.readyState==4 && this.status==200) {
callback(this);
} else {
// todo: Handle error.
}
}
}
requestSomeDataAndCall("http://example.com/foo", function(res){
setTimeout(function(){
var data = JSON.parse(res.responseText);
setTimeout(function(){
var price = data.price;
setTimeout(function(){
print("The price is "+price);
},10);
},10);
},10);
});
As Norbert Hartl pointed out, JSON.parse() will hang the browser for large strings. So I used setTimeout() to delay its execution (after a pause of 10 milliseconds). This is one example of Kris Kowal's solution. It allows the current Javascript thread to complete, freeing up the browser to present DOM changes and scroll the page for the user, before the callback runs.
I hope the commonjs promise framework also uses something like setTimeout, otherwise the later promises in the article's example will indeed run synchronously as feared.
My alternative above looks pretty ugly, with the later processes requiring further indentation. I restructured the code, so that we can provide our process chain all in one level:
function makeResolver(chain) {
function climbChain(input) {
var fn = chain.shift(); // This particular implementation
setTimeout(function(){ // alters the chain array.
var output = fn(input);
if (chain.length>0) {
climbChain(output);
}
},10);
}
return climbChain;
}
var processChain = [
function(response){
return JSON.parse(response.body);
},
function(data){
return data.price; // get the price
},
function(price){
print("The price is " + price);
}
];
var climber = makeResolver(promiseChain);
requestSomeDataAndCall("http://example.com/foo", climber);
I was hoping to demonstrate that traditional forward-passing of callbacks in Javascript is pretty much equivalent to promises. However after two attempts I appear to have shown, with reference to the neatness of the code in the original example, that promises are a far more elegant solution!
The second snippet is vulnerable to denial of service attack because example.com/foo can just return invalid json to crash the server. Even empty response is invalid JSON (though valid JS). It's like mysql_*
examples with glaring SQL injection holes.
And the promise code can be improved much as well. These are equal:
requestSomeData("http://example.com/foo") // returns a promise for the response
.then(function(response){ // ‘then’ is used to provide a promise handler
// parse the body
var data = JSON.parse(response.body);
// get the price
var price = data.price;
// print out the price
print("The price is " + price);
});
And:
requestSomeData("http://example.com/foo")
.requestHandler(function(response){
try {
var data = JSON.parse(response.body);
}
catch(e) {
return;
}
// get the price
var price = data.price;
// print out the price
print("The price is " + price);
});
If we wanted to handle the error, then these would be equal:
requestSomeData("http://example.com/foo") // returns a promise for the response
.then(function(response){ // ‘then’ is used to provide a promise handler
// parse the body
var data = JSON.parse(response.body);
// get the price
var price = data.price;
// print out the price
print("The price is " + price);
}).catch(SyntaxError, function(e) {
console.error(e);
});
and:
requestSomeData("http://example.com/foo")
.requestHandler(function(response){
try {
var data = JSON.parse(response.body);
}
catch(e) {
//If the above had a typo like `respons.body`
//then without this check the ReferenceError would be swallowed
//so this check is kept to have as close equality as possible with
//the promise code
if(e instanceof SyntaxError) {
console.error(e);
return;
}
else {
throw e;
}
}
// get the price
var price = data.price;
// print out the price
print("The price is " + price);
});
One might also add that the advantage of the first version over the second is that it separates different operations in the refinement chain (the functions don't have to be written in-place either). The second version mixes both the low-level parsing with application logic. Specifically, using the SOLID principles as guidelines, the second version violates both OCP and SRP.
精彩评论