原創性聲明:本文完全為筆者原創,請尊重筆者勞動力。轉載務必注明原文地址。
發現有一段時間沒有寫文章了,中了一段時間農藥的毒,后面都變得有點懶了。
今天寫寫javascript
中的這個Promise
,還記得第一次碰到這個東西是好久以前在angularJS
的環境下。angularJS
的$resource
獲取了后端RESTfull api
接口的數據后是一個Promise
對象,直接用雙向綁定到DOM上是能正確顯示的,但是控制臺打印它里面的某個屬性值死活就是undefined
,后來磕磕碰碰摸索久了就知道有個then
,今天再來總結一下這個東西。
前戲
Promise
譯為保證。表示異步處理的方法承諾返回一個值。Promise
對象也是用于解決異步中晦澀復雜的回調現象。至于如何解決,不妨先看看在Promise
之前,一個異步回調的例子:
var getData = function(url, callback) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
callback(req);
};
req.send();
}
var url = 'api/user/username/:id';
getData(url, function(req) {
if (req.status == 200) {
$('#username').text(req.statusText); // 用jquery將請求的數據寫入dom
} else {
console.error(new Error(req.statusText);
}
});
如果拿到username
后不是顯示這么簡單,還需要把username
作為請求參數繼續請求別的數據(例如:age),然后展示age
呢。
實際情況當然是直接請求一個user就行了,這里只是舉例方便。
改動一下代碼:
var getData = function(url, callback) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
callback(req.responseText);
};
req.send();
}
var url = 'api/user/username/:id';
getData(url, function(data) {
});
var url = 'api/user/username/:id';
getData(url, function(req) {
if (req.status == 200) {
var url = 'api/user/age/:username';
getData(url, function(req) {
if (req.status == 200) {
$('#age').text(req.statusText); // 用jquery將請求的數據寫入dom
} else {
console.error(new Error(req.statusText));
}
});
} else {
console.error(new Error(req.statusText));
}
});
顯然,這看起來不僅有點理解費勁,而且代碼也不好看。但是,要用回調實現這里面的異步請求,只能這樣干。事實上,這是一種強行異步變同步的辦法,通過回調,以確保數據拿到之后再做下一步的處理,所以這是事實上的同步。如果用Promise
,會怎么樣呢?
var getData = function(url) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status == 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.send();
});
}
var url = 'api/user/username/:id';
getData(url).then(function(username) {
var url = 'api/user/age/:username';
getData(url).then(function(age) {
$('#age').text(age); // 用jquery將請求的數據寫入dom
}, function(error) {
console.log(error);
});
}, function(error) {
console.log(error);
})
乍一看,代碼也沒有少很多嘛。的確,但是仔細理解這兩段代碼時,就會感受到它們的區別。采用Promise
時,調用getData
時你不需要關心它內部的代碼,給你的感覺就是getData(url)
已經返回了我想要的結果(username
),于是自然地就在then()
方法中的第一個參數(匿名函數)中直接用這個結果(username
)去發起下一個請求(請求age
),而在then()
的第二個參數(匿名函數)中對錯誤進行處理。
而采用回調方法,你總是需要去把該參數函數帶入getData
中。雖然它代碼的順序看上去和Promise
很像,但代碼執行的流程卻是離不開回調的本質。如果回調多了,就可能會陷入回調地獄。理解和維護都會很蛋疼。
Promise
上面的例子就基本闡明了Promise
的簡單用法。構建一個Promise
對象可以這樣:
var promise = new Promise(function(resolve, reject) {
// 異步處理,處理結束后、調用resolve 或 reject
});
然后就可以調用Promise
實例的then
方法:
promise.then(onFulfilled, onRejected);
onFulfilled
, onRejected
是兩個參數函數。
-
resolve
(成功)時:
onFulfilled
會被調用 -
reject
(失敗)時
onRejected
會被調用
也就是說,promise
執行成功(一般是后端交互請求數據),那么就會調用onFulfilled
方法,所以在這個方法里繼續寫拿到數據后要執行的代碼即可,如果發生了錯誤(例如:后端報錯),則會執行onRejected
方法,在里面定義錯誤處理的代碼即可。
上面涉及
Promise
的兩個API
方法:1. 構造函數Promise()
; 2.實例方法then()
。
怎么把一個常量包裝為Promise
對象?
var promise = Promise.resolve(99);
當然,這個對象的then
方法就完全沒有第二個函數參數的必要了。
說到第二個函數參數onRejected
,它是用于發生異常時被調用的。此外,還可以通過catch
來捕獲異常,如下:
var promise = Promise.resolve(99);
promise.then(function(value){
console.log(value); // 99
}).catch(function(error){
console.log(error);
})
與onRejected
效果一致。當然,此處的catch
也沒有意義。
Promise.resolve(99)
可以認為是下面代碼片段的語法糖:
Promise.resolve(99).then(function(value){
resolve(value); // 此時,這個promise對象立即進入確定(即resolved)狀態,并將99傳遞給后面then里所指定的 onFulfilled 函數
});
這里涉及了
Promise
的一個靜態方法:resolve
,除構造方法之外的另一個構建Promise
的方法。
Promise的狀態
用new Promise 實例化的promise對象有以下三個狀態。
has-resolution
-Fulfilled
(譯文:履行)
resolve(成功)時。此時會調用 onFulfilledhas-rejection
-Rejected
(譯文:拒絕)
reject(失敗)時。此時會調用 onRejectedunresolved
-Pending
(譯文:未決定)
既不是resolve
也不是reject
的狀態。也就是promise對象剛被創建后的初始化狀態等。
promise
對象的狀態,從Pending
轉換為Fulfilled
或Rejected
之后, 這個promise
對象的狀態就不會再發生任何變化。也就是說,Promise
與Event
等不同,在.then
后執行的函數可以肯定地說只會被調用一次。另外,Fulfilled
和Rejected
這兩個中的任一狀態都可以表示為Settled
(不變的,穩妥的:表示這個Promise的狀態已經回歸穩定,即完成了Fulfilled
或Rejected
)。
Thenable對象
什么是Thenable
對象?從名字上看,就是可以then
的對象——可以調用then
方法的對象。除了Promise
還有對象可以調用then
方法嗎?答案是肯定的,比如:
$.ajax('/json/comment.json'); // 可以繼續`.then()`
這是jquery的jqXHR
對象,它其實就是一個Thenable
對象。但它不是一個Promise
,因為不符合ECMAScript 6
中的標準。
如何將它轉換為一個標準的Promise
對象呢?
var promise = Promise.resolve($.ajax('/json/comment.json'));// => promise對象
promise.then(function(value){
console.log(value);
});
- 簡單總結一下 Promise.resolve 方法的話,可以認為它的作用就是將傳遞給它的參數填充(Fulfilled)到promise對象后并返回這個promise對象。
- 此外,Promise的很多處理內部也是使用了 Promise.resolve 算法將值轉換為promise對象后再進行處理的。
Promise方法鏈(promise chain
)
前面,接觸了then().catch()
這種鏈式調用的方式,其實Promise
的鏈式調用更加豐富:
function taskA() {
console.log("Task A");
}
function taskB() {
console.log("Task B");
}
function onRejected(error) {
console.log("Catch Error: A or B", error);
}
function finalTask() {
console.log("Final Task");
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejected)
.then(finalTask);
結果顯而易見:
Task A
Task B
Final Task
上面的代碼的執行流程用圖來展示,就像這樣:
顯然,
catch
會處理TaskA
,也會處理TaskB
。那如果TaskA
發生了錯誤,還會執行TaskB
嗎? 答案是不會。如下圖:
這時,我們再回到文章最開頭的那個Promise
例子,先根據id
獲取username
,再根據username
獲取age
,最后寫入dom中,這個過程顯然對于promise chain
鏈式調用的應用場景再符合不過了:
var getData = function(url) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status == 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.send();
});
}
var url = 'api/user/username/:id';
getData(url)
.then(function(username) {
var url = 'api/user/age/:username';
return getData(url); // a. 向下一個then中傳遞參數
}).then(function(age) {
$('#age').text(age);
}).catch(function(err){
console.log(err);
});
我們是怎樣往第二個
then
傳遞異步查詢的結果age
呢?通過一個關鍵的return
!
通過連續的then
方法調用,整個流程(兩個步驟)無論在理解上,還是代碼美觀上,都變得非常優雅!甚至讓你在編寫它的時候忘記它原來是個異步的過程——這簡直和寫同步代碼是一樣一樣的嘛。
另外,我們還注意到,原本應該有兩個catch
變成了一個catch
,倒金字塔
的惡心沒有了,滿屏的花括號也沒有了。流程變得清晰。這就是使用Promise
的良好開發體驗。
【疑問】:稍微細心點,不難發現
(a)
處return
的是getData(url)
,這個結果應該是一個Promise
對象,而不是一個普通值(例如: 24(歲)),為什么這樣也可以在下一個then
中通過age
參數接收到呢?經過實踐測試,返回普通值
或Promise
對象,都可以正常接收。
【結論】:每個方法中 return 的值不僅只局限于字符串或者數值類型,也可以是對象或者promise對象等復雜類型。
事實上,在每次調用then
方法后,都會返回一個新的Promise
對象,以供下一次鏈式調用。如果then
中匿名函數返回的是普通類型,Promise
在內部也會將其轉換為一個新的Promise
對象(注意:是新的Promise
對象)。
如上圖:兩個promise object
一定是兩個不同的Promise
對象,盡管then
中可能什么也沒有做,他們resolve之后的值也盡管可能相同。
由此,可能引出一個可能常犯的錯誤:
不正確的:
function asyncCall() {
var promise = Promise.resolve();
promise.then(function() { // a
// 任意處理
return newVar;
});
return promise;
}
(a)
處調用then
時,返回的是一個新的Promise
對象,而不再是promise
那個變量,因此這個then
中的異常是不會被外部捕獲的,而且它的返回值也無法在外部得到。
正確的:
function asyncCall() {
var promise = Promise.resolve();
return promise.then(function() {
// 任意處理
return newVar;
});
}
或者
function asyncCall() {
var promise = Promise.resolve();
var promise1 = promise.then(function() {
// 任意處理
return newVar;
});
return promise1;
}
Promise.all
方法
這個方法之前沒有提到過。仍以最開始案例延伸。現在我們不需要根據username
獲取age
了,可以直接根據id
獲取age
,也就是說我們有兩個請求:1. 根據id獲取username
,2. 根據id獲取age
。我們希望這兩個請求都完成后,再將結果username
和age
插入到DOM中。這時,Promise.all
就可以用上了:
var getData = function(url) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status == 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.send();
});
}
var urlUsername = 'api/user/username/:id';
var urlAge = 'api/user/age/:id';
var requestArr = [getData(urlUsername), getData(urlAge)]; // 兩個Promise對象組成的數組
Promise.all(requestArr).then(function(data) { // a. data的值是什么呢?
var username = data[0]; // b
var age = data[1]; // c
$('#username').text(username);
$('#age').text(age);
}).catch(function(err) {
console.log(err);
});
可以看出,Promise.all
的參數是一個數組,這個數組的成員是Promise
對象。有點像把多個異步請求放到一個Promise
里處理的感覺,這樣做的好處是一來代碼簡潔了一些,更重要的是(a)
處的then
方法能確保在兩個請求完成后再執行,也就是兩個Promise
對象的狀態都變成了Settled
。
Promise.all 在接收到的所有的對象promise都變為 FulFilled 或者 Rejected 狀態之后才會繼續進行后面的處理
。
那(a)
處的data
又是什么值呢?username
,or age
?
顯然從(b)
和(c)
的代碼可以看到data
是個數組,對應于requestArr
中兩個Promise
對象所代表的值(順序也是保持一致的)。
用
Promise.all
處理的多個Promise
不是按順序執行的,而是同時、并行執行。也就是說上面的兩個請求是同時、并發進行的,而不是請求了username
,再請求age
。
Promise.race方法
理解了Promise.all
,再看Promise.race
就簡單了。Promise.race
的用法和all
一樣,也是接收一個Promise
對象組成的數組,resolve
后的值(data
)格式也是一樣的。不同之處在于:
Promise.race只要有一個promise對象進入 FulFilled 或者 Rejected 狀態的話,就會繼續進行后面的處理
。
同樣以上面那個例子。如果將all
改為race
,那么只要有一個promise
先Settled
,then
就會開始執行,而其他promise
就不會繼續執行then
方法了。同樣then
中接收到的data,也不再是數組,而是先Settled
的那個Promise
對象resolve
的值。
從race
這個意思,也能大致看出它的這個特性。
Deferred和Promise
Deferred
譯為延期。它和Promise
不同,它沒有共通的規范,換句話說,在ECMAScript
內容里有對Promise
的定義標準,但是沒有Deferred
的定義標準,更通俗的說,你用javascript
語言可以直接像用Math
、Date
那樣直接在代碼里去用Promise
,但是Deferred
是不存在的。它的存在純粹是市面上各種類庫實現出來的。我們自己也可以簡單實現一個Deferred
類:
deferred.js
function Deferred() {
this.promise = new Promise(function (resolve, reject) {
this._resolve = resolve;
this._reject = reject;
}.bind(this));
}
Deferred.prototype.resolve = function (value) {
this._resolve.call(this.promise, value);
};
Deferred.prototype.reject = function (reason) {
this._reject.call(this.promise, reason);
};
上面的 Function Deferred
就是我們實現的Deferred
類。熟悉bind
、call
和原型繼承
的話,不難理解上面代碼的含義:新建了一個對象Deferred,它有一個屬性promise, 內部構建了一個Promise實例,把這個實例的一些api方法通過原型繼承給了Deferred,Deferred可以通過調用自己的resolve或reject等方法控制其屬性promise的狀態
。
為什么要這么麻煩呢?
為了更好的操作
Promise
。
比如,在之前的案例中,Promise
什么時候resolve
或reject
,我們通常只能在Promise
構造函數中調用。現在用上剛寫的Deferred
,會顯得更靈活,我們改造一下getData
:
var getData = function(url) {
var deferred = new Deferred();
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status == 200) {
deferred.resolve(req.responseText);
} else {
deferred.reject(new Error(req.statusText));
}
};
req.send();
return deferred.promise;
}
var url = 'api/user/username/:id';
getData(url).then(function(data) {
$('#username').text(data);
}).catch(function(err) {
console.log(err);
});
我們看到getData
里面已經看不到Promise
的構造函數了,用defered可以自由調用resolve
和reject
。試想,我們有一個Promise
對象,我們不知道它的狀態是否已經Settled
,我們可以用Deferred
手動resolve
一下:
var promise = ... ;// 一個來源于其他地方的promise
var deferred = new Deferred();
deferred.resolve(promise);
return deferred.promise;
返回的deferred.promise
狀態是確定的。
所以,其實Deferred
并不抽象,若要說抽象,那就是對call
、bind
和javascript 原型繼承
的理解還不透徹。很多類庫都有實現Deferred
這個東西,只是大家都約定俗成用Deferred
這個名字,例如jquery
、angular
,而且各類庫的實現方式未必相同。
Deferred
和Promise
并不是處于競爭的關系,而是Deferred內涵了Promise,也就包裝了一下。
【個人觀點】:就像Promise
也是包裝了一下resovle
之后的具體值,附加一些api方法,避開了繁雜的回調,提升了開發體驗和閱讀代碼體驗。這讓我想起了java 8
中的Optional
,它也是對一個對象的包裝,提供一些api方法,避免了繁雜的非空判斷,提升了開發體驗和代碼的優雅性。
【參考】: