拥抱javascript的Promise

来源:互联网 发布:Linux主分区大小 编辑:程序博客网 时间:2024/04/20 12:34

原文地址:http://javascriptplayground.com/blog/2015/02/promises/?utm_source=javascriptweekly&utm_medium=email

这篇文章我们一起来看下在异步编程事怎么拥抱promise,编写良好的代码。这篇不是全面深入地剖析Promise,如果想更全面的了解,Jake Archibald’s post on HTML5 Rocks覆盖了方方面面。 强烈建议你阅读下。

尽管这篇文章中的代码依赖es-6-promise 库,它却是将要实现的ES6中一种补充。所有代码都通过运行nodejs执行,但是它应当有运行在浏览器环境的能力。当代码运行到Promise的时候,将会启用上面的es6-promise库,但是如果浏览器已经广泛地实现了promise特性,那么这些代码也可以同样的执行。

处理errors

第一个特性就是处理错误信息。这也是很多人都曾经问过的,很多人都遇到过的理解上陷阱。看下下面的代码,当执行这段代码的时候,你想会输出什么?

var someAsynThing = function(){
return new Promise(function(resolve,reject){
resolve(x +2 );
});
}
someAsynThing().then(function(){
console.log(‘Everything is Ok’);
});
你可能想的是抛出一个错误,因为变量X没有定义。如果你把代码写在promise外,就会发生你期望的情况。然而这块代码却相安无事,控制台什么都没有输出,并且也没有抛出任何错误。在promise内部,任何的错误都被和谐了,被理解为promise rejecting,这就意味着我们需要处理下error的情况。

someAsynThing().then(function(){
console.log(‘Everything is Ok’);
}).catch(function(error){
console.log(‘oh no’,error);
});
运行下代码,

ReferenceError: x is not defined

你也可以通过使用promise链使errors看起来比较舒服,看下下面的实例代码

var someAsynThing = function(){
return new Promise(function(resolve,reject){
resolve(x +2 );
});
}
var someOtherAsynThing = function(){
return new Promise(function(resolve,reject){
reject(“Something went wrong”);
});
}
someAsynThing().then(function(){
return someOtherAsynThing();
}).catch(function(error){
console.log(‘oh no’,error);
});
我们依然看到同样的错误 oh no [ReferenceError: x is not defined],因为someAsynTing rejected,如果这个方法resolves,我们就会看到someOtherAsynThing reject(即“Something went wrong”)

var someAsynThing = function(){
return new Promise(function(resolve,reject){
var x =2;
resolve(x +2 );
});
}
var someOtherAsynThing = function(){
return new Promise(function(resolve,reject){
reject(“Something went wrong”);
});
}
someAsynThing().then(function(){
return someOtherAsynThing();
}).catch(function(error){
console.log(‘oh no’,error);
});
现在我们看到的是oh no something went wrong.当一个promise rejects,promise链的第一个catch就会被调用。

另一个重要点就是没有特别的东西注册给catch。当一个promise rejects时只是一个一般的方法进行处理,而不会阻止其他的错作。

someAsynThing().then(function() {
return someOtherAsynThing();}).catch(function(error) {
console.log(‘oh no’, error);}).then(function() {
console.log(‘carry on’);}
);
一但rejects,“carry on”将会打印出来。当然catch内的handle会抛出一个错误,

someAsynThing().then(function() {
return someOtherAsynThing();
}).catch(function(error) {
console.log(‘oh no’, error);
y +2;
}).then(function() {
console.log(‘carry on’);
});
这时catch中的回调方法被调用,但是不再显示“carry on”,因为catch中抛出一个错误。如果在promise链结束处增加一个catch方法,这个catch会继续运行,因为在一个catch中抛出,下一个catch将会调用。

链接和各种承诺传递

这部分是从我最近的工作受到的启发,导出CSV文件。做这个功能使用angularjs内置的$q服务,在这里做一个简单介绍。

导出CSV文件(CSV是浏览器内置的格式,借助FileSaver)的步骤

1、通过API接口读取数据来组装CSV(也可能是多个API请求)

2、将数据传递给CSV初始化对象

3、向CSV文件写入数据

4、文件创建成功或失败都要给用户一个确认消息

我们不会详细介绍代码怎么实现,只是从更高的层次的介绍下使用Promise构建健壮的解决方案。业务复杂的处理,错误随时都会发生(api加载,数据转换,或者CSV没有正确保存)。我们使用Promise的then和catch后才会发现这种处理方式更加的优雅。

正如你看到的,promise很笨重地结束了。但是我个人认为Promise链使得代码非常优美,但是刚开始你觉得有些古怪,Jake Archibald 能帮你的忙,让这种实现更好:

当从then回调中返回什么时候时,感觉很神奇。如果返回一个值,下一个then会被调用(这个值会被传递过去)。如果返回一个promise,下一个then将会等待,只有等前面的promise success/fails时,这个then中的回调才会被调用。

再次强调下,如果想更加深刻地了解promise,强烈建议你了解下http://www.html5rocks.com/en/tutorials/es6/promises/

现在我们开始实现一个简单的例子,返回写简单的数据。真实的application中可能是http请求或者别的类型。我们的promise讲resolve一个数组,将这些数据导出到CSV。

var fetchData = function() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve({
users: [
{ name: ‘Jack’, age: 22 },
{ name: ‘Tom’, age: 21 },
{ name: ‘Isaac’, age: 21 },
{ name: ‘Iain’, age: 20 }
]
});
}, 50);
});}
接下来,需要一个function准备CSV 数据,立即resolve数据,但是在真实的application中需要做更多的work。

var prepareDataForCsv = function(data) {
return new Promise(function(resolve, reject) {
// imagine this did something with the data
resolve(data);
});};
这里我们还有很多注意点:prepareDataForCsv 不是异步的,并且没有必要把它封装到promise中。但是一个function作为操作链的一部分时,我们发现包装到一个promise内带来很大的好处,因为所有的error都可以通过promise处理。

最后,需要提供一个function向CSV写入数据,

var writeToCsv = function(data) {
return new Promise(function(resolve, reject) {
// write to CSV
resolve();
});};
现在,我们将它们组合在一起。

fetchData().then(function(data) {
return prepareDataForCsv(data);}).then(function(data) {
return writeToCsv(data);}).then(function() {
console.log(‘your csv has been saved’);});
这种实现相当简洁,并且执行流程很清晰,我们还可以整理下更加紧凑。如果有一个方法带有一个参数,就可以将方法名直接传进去,而不是在回调方法中调用。

fetchData().then(prepareDataForCsv).then(writeToCsv).then(function() {
console.log(‘your csv has been saved’);});
底层的实现确实很是复杂,但是高层的API确实很好。我越来越欣赏promise,一但使用他们(complex code )并且成功运行,但是可以使用看起来比较优美的代码让这些复杂的实现结束。

然而,现在我们不需要error处理,但是我们可以增加一段额外代码。

fetchData().then(prepareDataForCsv).then(writeToCsv).then(function() {
console.log(‘your csv has been saved’);}).catch(function(error) {
console.log(‘something went wrong’, error);});
正是因为promise的作用链和error是怎么工作的,前面已经讨论,作用链的结束处的catch能够捕捉到任何的异常,使得error的处理径直向前。

为了说明这个,改造下prepareDataForCsv方法:

var prepareDataForCsv = function(data) {
return new Promise(function(resolve, reject) {
// imagine this did something with the data
reject(‘data invalid’);
});};
现在运行代码记录下错误。这是相当真棒 – prepareDataForCsv是正确的,在我们的promise 链的中间,但我们没有做任何额外的工作或弄虚作假处理错误。此外,catch不仅会捕捉我们通过promise rejects 的错误,但也抛出异常。这意味着,即使真是出乎意料边缘的情况下触发一个JS异常,用户仍然有自己的错误处理,符合预期结果。

我发现另一个非常强大的办法是期待一些数据,而不是使用promise resolve一些数据。让我们prepareDataForCsv为例:

var prepareDataForCsv = function(dataPromise) {
return dataPromise().then(function(data) {
return data;
});};
我们发现这是一个相当不错的模式用来整理代码,并保持它更通用,其中大部分的工作是异步传递promise,而不是等着他们resolve和传送数据。

修改后的代码,如下:

prepareDataForCsv(fetchData).then(writeToCsv).then(function() {
console.log(‘your csv has been saved’);}).catch(function(error) {
console.log(‘something went wrong’, error);});
这样做的好处是,错误处理并没有改变。 fetchData可以reject某种形式,并且该错误将仍然在最后捕获处理。一旦你认同了这一点,你会发现promise非常好用,甚至更好的处理错误。

Promise递归

其中一个我们不得不面对的问题是,有时得从我们的API获取数据,可能需要进行多次请求。这是因为分页请求,所以如果数据量很大,你需要请求多次。值得庆幸的是我们的API告诉你,是否获取更多的数据,在此节中,我将解释我们是如何与Promise配合使用递归加载所有这些数据。

var count = 0;var http = function() {
if(count === 0) {
count++;
return Promise.resolve({ more: true, user: { name: ‘jack’, age: 22 } });
} else {
return Promise.resolve({ more: false, user: { name: ‘isaac’, age: 21 } });
}};
首先,我们有HTTP,这将作为假HTTP调用我们的API。 (Promise.resolve只是创建了一个promise,不管你是怎么调用都会resolve)。我第一次创建请求,将标志设置为true,这表明有更多的数据获取(这并不是现实中API怎么响应回应,但这篇文章的目的也是为了这个目的)。第二次该请求将more字段设置为false。因此,要获取所有需要的数据,我们需要两个API调用。让我们写一个函数fetchData来处理:

var fetchData = function() {
var goFetch = function(users) {
return http().then(function(data) {
users.push(data.user);
if(data.more) {
return goFetch(users);
} else {
return users;
}
});
}

return goFetch([]);};
fetchData本身确实非常短小,除了定义,然后调用另一个函数,goFetch。 goFetch返回一个用户数组(以goFetch初始话传递一个空数组),然后调用HTTP(),它resolve一组数据。返回的新的用户被存入用户数组,然后将功能着眼于data.more字段。如果这是真的,它再次调用自身,传递用户的新数组。如果它是假的,不在调用,它只是返回用户数组。这里最重要的事情,这么实现的原因是,在每一个阶段的东西被返回。 fetchData返回goFetch,它要么返回本身或用户数组。

结论

承诺不是随处可见,但是会成为处理大量异步操作的标准方法。然而,我发现其中的操作,有些是同步的,有些是异步的,一般提供了很多便利。如果你还没有尝试过但我真的建议在你的下一个项目试试。

0 0