擁抱javascript的Promise

原文地址: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,它要麼返回本身或用戶數組。

結論

承諾不是隨處可見,但是會成爲處理大量異步操作的標準方法。然而,我發現其中的操作,有些是同步的,有些是異步的,一般提供了很多便利。如果你還沒有嘗試過但我真的建議在你的下一個項目試試。

發佈了60 篇原創文章 · 獲贊 22 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章