JavaScript非同步之Promise
傳統的JavaScript非同步通常基於回撥實現,但回撥方式有兩個重要的缺點:
不便於除錯:由於回撥函式是基於事件佇列實現的,當回撥方法條用時,其外部呼叫函式並不在函式執行棧中,這給debug帶來了極大不便。來看下下面這個例子:
function init(name) { test(name) } setTimeout(function A() { setTimeout(function() { init(); }, 0); }, 0);
可以看到,setTimeout並未出現在異常堆疊中
- 回撥地獄:在非同步程式設計中,通常會出現回撥巢狀的場景。一層層回撥相互巢狀,稱為回撥地獄。嚴重影響程式碼可讀性
Promise
由於傳統回撥的諸多缺點,Promise
被提出以一種更友好的方式解決上述問題。Promise
是什麼呢?簡單來說,Promise
是一個封裝未來事件結果的可複用非同步任務管理機制。從這個定義中,我們可以看到Promise
的幾個主要特點:
- 非同步:
Promise
是用於描述未來事件的,未來事件什麼時候發生並不知曉,因而其必然是基於非同步實現的 - 任務管理:當未來事件發生後,如何處理未來事件?未來事件成功如何處理?失敗又如何處理?所以
Promise
還涉及到任務管理 - 可複用:一個未來事件可能有多個回撥處理,同時非同步任務也可能是多重巢狀的,即非同步任務回撥中還巢狀著另一個非同步任務。所以
Promise
術語
首先來看下Promise
下幾個常用術語:
- Promise:指一個擁有符合規範的then方法的物件
- thenable:指一個定義了then方法的物件
- resolve:改變一個promise物件從等待狀態到已完成或拒絕狀態,一旦改變,不可再改
- reject reason:拒絕原因
另外,一個Promise
中還有三個狀態:
- pending:等待、初始狀態
- fullfilled:已完成,未來事件操作成功
- rejected:已拒絕,未來事件操作失敗
一個Promise
物件的狀態變化只能有如下兩種:
pending -----> fullfilled
或
pending ------> rejected
THEN
方法
Promise
所提供的,用於訪問未來事件處理結果的方法:
Promise.then(onFulfilled, onRejected)
/*
* - 兩個引數均為可選,均有預設值,若不傳入,則會使用預設值;
* - 兩個引數必須是函式,否則會被忽略,使用預設函式;
* - onFulfilled: 在promise已完成後呼叫且僅呼叫一次該方法,該方法接受promise最終值作引數;
* - onRejected: 在promise被拒絕後呼叫且僅呼叫一次該方法,該方法接受promise拒絕原因作引數;
* - 兩個函式都是非同步事件的回撥,符合JavaScript事件迴圈處理流程
*/
Resolution
Promise
的核心就是一個resolution
的過程,即處理未來事件,並確定事件成功或失敗的條件,並在對應條件下執行onFullfilled
或 onRejected
(由then方法傳入)方法通知呼叫方。
接下來看一個Promise
的例子:
let myPromise = new Promise((resolve, reject) =>{
setTimeout(function(){
console.log('resolve');
resolve('success')
}, 1000 * 3)
});
myPromise.then((msg) => {
console.log("Yay!" + msg);
});
console.log("after execute promise");
對應的輸出為:
after execute promise
resolve
Yay!success
從輸出可以看出其非同步特性:Promise
的例項化以及then
方法都不是阻塞式函式,javascript
依然繼續向下執行,所以最先輸出的便是after execute promise
。
(resolve, reject) =>{
setTimeout(function(){
console.log('resolve');
resolve('success')
}, 1000 * 3)
}
初始化Promise
物件時傳入的處理函式是Promise
的核心,如上述程式碼所示,在該Promise
物件中設定一個3S的定時器。3S秒後,任務執行成功,所以通過呼叫resolve
將成功資訊透出,同時resolve
方法又會通過onResolved
方法(即在then
方法中傳入的處理函式)將該資訊透出給呼叫者。至此,一個完整的Promise
流程執行完畢。其中resolve
reject
方法由Promise
提供,使用者執行指定何時呼叫該方法即可。
接下來再來看一個例子:
let myPromise = new Promise((resolve, reject) =>{
setTimeout(function(){
console.log('resolve');
resolve('success')
reject('failed')
}, 1000 * 3)
});
myPromise.then((msg) => {
console.log(msg)
console.log("Yay!" + msg);
return 'first promise'
}, (reason) => {
console.log('rejected:' + reason)
});
對應的輸出為:
resolve
success
Yay!success
可以看到,儘管同時呼叫了resolve
和reject
,但只有resolve
被執行了,這也再次驗證了Promise
的狀態不可變性:即Promise的狀態一旦變為resolved
或rejected
便不會再改變。
接下來再改變下上述程式碼:
let myPromise = new Promise((resolve, reject) =>{
setTimeout(function(){
console.log('resolve');
resolve('success')
resolve('success')
}, 1000 * 3)
});
myPromise.then((msg) => {
console.log(msg)
console.log("Yay!" + msg);
return 'first promise'
}, (reason) => {
console.log('rejected:' + reason)
});
resolve
success
Yay!success
可以看到,儘管呼叫了兩次resolve
方法,但onResolve
方法只執行了一次,即當promise
物件的狀態一旦變為resolved
或是rejected
後,便不再執行resolve
或reject
方法。
看完了上述的例子,我們重新來看下resolve
和reject
方法:
Promise.resolve(x)
/*resolve方法返回一個已決議的Promsie物件:
若x是一個promise或thenable物件,則返回的promise物件狀態同x;
若x不是物件或函式,則返回的promise物件以該值為完成最終值;
否則,詳細過程依然按前文Promsies/A+規範中提到的規則進行。*/
Promsie.reject(reason)
/*返回一個使用傳入的原因拒絕的Promise物件。*/
Promise.prototype.then
看完了resolve
方法和reject
方法,接下來來看下then
方法:
該方法為promsie新增完成或拒絕處理器,將返回一個新的promise,該新promise接受傳入的處理器呼叫後的返回值進行決議;若promise未被處理,如傳入的處理器不是函式,則新promise維持原來promise的狀態。
來看下下面這個例子:
var promise = new Promise((resolve, reject) => {
setTimeout(function() {
resolve('success');
}, 10);
});
promise.then((msg) => {
console.log('first messaeg: ' + msg);
}).then((msg) => {
console.log('second messaeg: ' + msg);
});
其輸出結果為:
first messaeg: success
second messaeg: undefined
可以看到,第一個then
方法成功接收到了resolve
方法返回的結果,但第二個then
方法接收到的卻是undefined
。這是為什麼呢?then
方法會返回一個promise
物件,並且該新promise根據其傳入的回撥執行的返回值,進行決議,而函式未明確return
返回值時,預設返回的是undefined
,這也是上面例項第二個then
方法的回撥接收undefined
引數的原因。
所以接下來我們隊上次上述程式碼進行修改:
var promise = new Promise((resolve, reject) => {
setTimeout(function() {
resolve('success');
}, 10);
});
promise.then((msg) => {
console.log('first messaeg: ' + msg);
return 'succss';
}).then((msg) => {
console.log('second messaeg: ' + msg);
});
對應的輸出就變為:
first messaeg: success
second messaeg: success
Promise.prototype.catch
catch
方法等同於then
方法中的onRejected
方法,為promise物件新增異常處理邏輯。
鏈式呼叫
正式由於then
方法返回一個promise物件,所以可以基於Promise實現鏈式回撥呼叫:
var fourthPromise = new Promise((resolve, reject) => {
setTimeout(()=>{
console.log('first promise')
resolve('first success');
},1000);
}).then((msg) => {
console.log(msg)
return new Promise((resolve, reject) => {
console.log('second promise')
setTimeout(() => {resolve('second success')}, 1000)
})
}).then((msg) => {
console.log(msg)
return new Promise((resolve, reject) => {
console.log('third promise')
setTimeout(() => {resolve('third success')}, 1000)
})
});
fourthPromise.then((msg) => {
console.log(msg)
})
對應的輸出為:
first promise
first success
second promise
second success
third promise
third success