前言
假如需要向後端發送一個請求,並對返回的數據進行操作,可能我們第一時間想到的是回調函數。但如果接着又需要執行第二個、第三個...第n個異步操作,那麼回調函數就會一層層的嵌套,嚴重影響了代碼可讀性和可維護性。
Promise就是解決這個問題的方案,Promise主要做的事情是把回調函數的嵌套邏輯替換成了符合正常人思維習慣的線性邏輯,本文主要介紹Promise的基本用法、API、鏈式操作、異常處理以及利用promise對數組進行異步操作的方法。
一、雜說
Promise是從DOM中的Futures引入javascript的,理由大概是出現了像NodeJs這樣獨立於瀏覽器之外的JavaScript運行環境。在Promise正式被實現之前,部分JS庫,像Q、when、WinJS、RSVP.js、jQuery都根據Promises/A+標準分別實現了略有差異的”類Promise“對象。如果你的項目中用到了這些庫,不用擔心,標準的Promise對象提供了將這些”類Promise“對象轉換爲標準Promise對象的方法(後文會提到)。關於Promise的兼容性,參考Can I Use。
Promise有三種狀態:pending、resolved、rejected,狀態之間的轉換隻能從pending到resolved或rejected,並且狀態一旦轉換就再也無法改變;
Promise的API:
Promise的構造器接受一個函數,這個函數接受兩個參數:resolved,rejected。
promise.then(onResolved, onRejected), 不做贅述;
promise.catch(onRejected), promise.then(undefined, onRejected)的語法糖。
Promise.resolve(argument),返回一個Promise對象,具體取決於它接受的參數類型。
參數爲一個Promise對象,直接返回這個對象;
參數爲一個“類promise”對象,將其轉化成真正的Promise對象並返回;
參數爲其他值,返回一個以參數值作爲其resolved函數參數的Promise對象;
Promise.reject(obj), 返回一個以參數值(Error的實例)作爲其reject函數參數的Promise對象;
Promise.all(array), 參數值爲Promise數組(也可以包含"類Promise"對象),對數組的每一項調用Promise.resolve(),全部成功則resolved並返回返回值的數組,否則返回第一個rejected的error對象;
Promise.race(array), 返回數組中最先resolved或者rejected的那個Promise對象的返回值或者error對象。
二、基本用法
Promise是一個JavaScript對象,它執行在未來的某個時刻才知道結果的操作並返回得到的值或者失敗的信息。
// Promise is something like this.
var promise = new Promise(function(resolved, rejected) {
doSomethingAsync();
if (success) {
resolved();
} else {
rejected();
}
})
//How to use a promise. First arg is resolved, second is rejected
promise.then(function(res) {
console.log(res);
}, function(err) {
alert(err);
})
三、鏈式調用
如果僅有一個Promise對象的話,情況較爲簡單,即在Promise對象被定義時異步操作就開始執行,我們關心的並不是它什麼時候執行完畢,而是要在它執行完或者返回錯誤後對結果進行處理。但當多個Promise要按照一定的順序執行時,事情就變得複雜起來了。
function fetchSomething() {
return new Promise(function(resolved) {
if (success) {
resolved(res);
}
});
}
fetchSomething().then(function(res) {
console.log(res);
return fetchSomething();
}).then(function(res) {
console.log('duplicate res');
return 'done';
}).then(function(tip) {
console.log(tip);
})
then函數始終返回一個promise對象,後續的then要等待返回的promise resolve後才能執行,這樣就實現了線性邏輯的鏈式調用。而返回的promise取決於then函數本身return的值。如果return值本身就是一個promise對象,則替代默認的promise對象作爲返回值;如果return值爲其他值,則將這個值作爲返回的promise的resolve函數的參數值。
四、異常處理
從上面的代碼可以看出,then函數接受兩個參數:resolved、rejected。上面沒寫rejected是因爲rejected函數是可選的,當然也可以在then之後寫catch,.catch(rejected)本質上是.then(undefined, rejected)的語法糖。
這兩種方式是有區別的,.then(resolved, rejected)只能捕獲之前的promise的異常,而寫在其後的.catch(undefined, rejected)還可以捕獲其resolved函數產生的異常。另外只要Promise鏈中有一個promise對象拋出異常,其後所有的resolved都被跳過,直到這個異常被rejected或者catch處理。
五、排序
當需要用數組的數據執行異步操作,因爲數組的遍歷方法forEach、map等都是同步的,所以結果的順序就取決於異步操作完成的順序,如果對順序有要求,這樣就不盡人意。
// 假設fetchID返回一個Promise對象
names.forEach(function(name) {
fetchID(name).then(function(id) {
renderInfo(id);
})
})
這個時候就需要利用then()來制定順序:
names.reduce(function(sequence, name) {
return sequence.then(function() {
return fetchID(name);
}).then(function(id) {
renderID(id);
})
}, Promise.then())
因爲此時先遍歷的name處理的結果將作爲後面的sequence,構成了鏈式關係,就避免了下載速度決定順序的問題。但仍然可以優化:因爲此時的ID是獲取一個,render一個的。如果能夠先獲取所有的ID再逐條渲染的話,性能會更好。
Promise.all(names.map(fetchID))
.then(function(IDs) {
IDS.forEach(function(id) {
renderID(id); //同步
})
})