Promise
物件,ES6
新增的一個全新特性,雖然它出現很久了,but
我相信,有很多的小夥伴還是沒有怎麼用過,今天讓我們來好好的學習一下它。
1、Promise 的含義
所謂Promise
,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作
)的結果。從語法上說,Promise
是一個物件,從它可以獲取非同步操作的訊息。
在日常開發中,經常需要用到ajax
請求資料,拿到資料後,再進行一些處理,完成一些複雜的互動效果。
假如你需要用ajax進行多次請求,而且,每次請求都依賴上一次請求返回的資料來作為引數,然後繼續發出請求,你寫成這樣,場景還原:
$.ajax({
success:function(res1){
$.ajax({
success:function(res2){
$.ajax({
success:function(res3){
}
});
}
});
}
});
複製程式碼
可能會有更多的巢狀,如此一層一層的執行,無疑是消耗了更多的等待時間,而且多個請求之間如果有先後關係的話,就會出現回撥地獄,ES6想了辦法整治這種情況,這時候Promise
誕生了。
所以我們知道了 Promise
是非同步程式設計的一種解決方案,比傳統的回撥函式和事件更合理和強大。
Promise
物件有以下兩個特點:
- 物件的狀態不受外界影響。
Promise
物件代表一個非同步操作,有三種狀態:pending
(進行中)、fulfilled
(已成功)和rejected
(已失敗)。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise
這個名字的由來。- 一旦狀態改變,就不會再變,任何時候都可以得到這個結果。
Promise
物件的狀態改變,只有兩種可能:從pending
變為fulfilled
和從pending
變為rejected
。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為resolved
(已定型)。如果改變已經發生了,你再對Promise
物件新增回撥函式,也會立即得到這個結果。
有了Promise
物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥地獄。
有好就有壞,Promise
也有一些缺點。1、首先,無法取消Promise
,一旦新建它就會立即執行,無法中途取消。2、其次,如果不設定回撥函式,Promise
內部丟擲的錯誤,不會反應到外部。3、當處於pending
狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
2、Promise 的基本用法
接下來,我們就看看它的基本用法:
const promise = new Promise(function(resolve, reject) {
});
複製程式碼
Promise
物件是全域性物件,你也可以理解為一個類,建立Promise
例項的時候,要有那個new
關鍵字。Promise
建構函式接受一個函式作為引數,其中有兩個引數:resolve
和reject
,兩個函式均為方法。resolve
方法用於處理非同步操作成功後業務(即從 pending
變為 resolved
)。reject
方法用於操作非同步操作失敗後的業務(即從 pending
變為 rejected
)。在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
上面也提到了,Promise
物件有三種狀態:
- 1、pending:剛剛建立一個
Promise
例項的時候,表示初始狀態; - 2、fulfilled:
resolve
方法呼叫的時候,表示操作成功; - 3、rejected:
reject
方法呼叫的時候,表示操作失敗; 下面程式碼創造了一個Promise
例項。
const promise = new Promise(function(resolve, reject) {
//例項化後狀態:pending
if (/* 非同步操作成功 */){
resolve(value);
// resolve方法呼叫,狀態為:fulfilled
} else {
reject(error);
// reject方法呼叫,狀態為:rejected
}
});
複製程式碼
初始化例項後,物件的狀態就變成了pending
;當resolve
方法被呼叫的時候,狀態就會由pending
變成了成功fulfilled
;當reject
方法被呼叫的時候,狀態就會由pending
變成失敗rejected
。
Promise
例項生成以後,可以用then()
方法分別指定resolved
狀態和rejected
狀態的回撥函式,用於繫結處理操作後的處理程式。
promise.then(function(value) {
// 操作成功的處理程式
}, function(error) {
// 操作失敗的處理程式
});
複製程式碼
then()
方法可以接受兩個回撥函式作為引數。第一個回撥函式是Promise
物件的狀態變為resolved
時呼叫,第二個回撥函式是Promise
物件的狀態變為rejected
時呼叫。其中,第二個函式是可選的,不一定要提供。這兩個函式都接受Promise
物件傳出的值作為引數。
說簡單點就是引數是兩個函式,第一個用於處理操作成功後的業務,第二個用於處理操作異常後的業務。
舉一個Promise物件的簡單例子。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'hello world');
});
}
timeout(100).then((value) => {
console.log(value); // hello world
});
複製程式碼
上面程式碼中,timeout()
方法返回一個Promise
例項,表示一段時間以後才會發生的結果。過了指定的時間(ms
引數)以後,Promise
例項的狀態變為resolved
,就會觸發then()
方法繫結的回撥函式。
Promise
新建後就會立即執行。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
複製程式碼
Promise
新建後立即執行,所以首先輸出的是Promise
。然後,then()
方法指定的回撥函式,將在當前指令碼所有同步任務執行完才會執行,所以resolved
最後輸出。
對於操作異常的程式,Promise
專門提供了一個例項方法來處理:catch()
方法。
catch()
只接受一個引數,用於處理操作異常後的業務。
getJSON('/posts.json').then(function(posts) {
// 處理成功
}).catch(function(error) {
// 處理 getJSON 和 前一個回撥函式執行時發生的錯誤
console.log('發生錯誤!', error);
});
複製程式碼
Promise
使用鏈式呼叫,是因為then方法
和catch方法
呼叫後,都會返回promise物件。
上面程式碼中,getJSON()
方法返回一個 Promise
物件,如果該物件狀態變為resolved
,則會呼叫then()
方法指定的回撥函式;如果非同步操作丟擲錯誤,狀態就會變為rejected
,就會呼叫catch()
方法指定的回撥函式,處理這個錯誤。另外,then()
方法指定的回撥函式,如果執行中丟擲錯誤,也會被catch()
方法捕獲。
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
// Error: test
複製程式碼
上面程式碼中,promise
丟擲一個錯誤,就被catch方法
指定的回撥函式捕獲。
如果 Promise
狀態已經變成resolved
,再丟擲錯誤是無效的。
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
複製程式碼
上面程式碼中,Promise
在resolve
語句後面,再丟擲錯誤,不會被捕獲,等於沒有丟擲。因為 Promise
的狀態一旦改變,就永久保持該狀態,不會再變了。
因為 Promise.prototype.then
和 Promise.prototype.catch
方法返回promise
物件, 所以它們可以被鏈式呼叫
注意
: 如果一個promise
物件處在fulfilled
或rejected
狀態而不是pending
狀態,那麼它也可以被稱為settled
狀態。你可能也會聽到一個術語resolved
,它表示promise
物件處於settled
狀態。
舉了這麼多栗子,要是沒看懂,讓我們串一串吧,梳理一下上面提到的。(認真的看註釋)
// 首先用 new 關鍵字建立一個 `Promise` 例項
const promise = new Promise(function(resolve, reject){
// 假設 state 的值為 true
let state = true;
if( state ){
// 呼叫操作成功方法
resolve('操作成功');
//狀態:從pending 到 fulfilled
}else{
// 呼叫操作異常方法
reject('操作失敗');
//狀態:從pending 到 rejected
}
});
promise.then(function (res) {
//操作成功的處理程式
console.log(res)
}).catch(function (error) {
//操作失敗的處理程式
console.log(error)
})
// 控制檯輸出:操作成功
複製程式碼
上面示例介紹了從 建立例項,狀態轉換,then方法和catch方法的使用。
如果多個操作之間層層依賴,我們用Promise
又是怎麼處理的呢?
const promise = new Promise(function(resolve, reject){
if( true ){
// 呼叫操作成功方法
resolve('操作成功');
//狀態:從pending 到 fulfilled
}else{
// 呼叫操作異常方法
reject('操作失敗');
//狀態:從pending 到 rejected
}
});
promise.then(a)
.then(b)
.then(c)
.catch(requestError);
function a() {
console.log('請求A成功');
return '請求B,下一個是誰';
}
function b(res) {
console.log('上一步的結果:'+ res);
console.log('請求B成功');
return '請求C,下一個是誰';
}
function c(res) {
console.log('上一步的結果:'+ res);
console.log('請求C成功');
}
function requestError() {
console.log('請求失敗');
}
複製程式碼
如圖所示:
上面的程式碼,先是建立一個例項,還宣告瞭4個函式,其中三個是分別代表著
請求A,請求B,請求C
;有了then
方法,三個請求操作再也不用層層巢狀了。我們使用then
方法,按照呼叫順序,很直觀地完成了三個操作的繫結,並且,如果請求B
依賴於請求A
的結果,那麼,可以在請求A
的程式用使用return
語句把需要的資料作為引數,傳遞給下一個請求,示例中我們就是使用return
實現傳遞引數給下一步操作的。
為了更直觀的看到示例所展示的情況,在下做了一張圖:
再舉個Promise 中微任務順序的栗子1:
var p = new Promise( (resolve, reject) => {
setTimeout( () => {
console.log('1');
}, 3000);
resolve(1);
}).then( () => { // 描述:.then() 1-1
Promise.resolve().then( () => { // 描述:.then() 2-1
Promise.resolve().then( () => { // 描述:.then() 3-1
console.log('2');
})
})
}).then( () => { // 描述:.then() 1-2
console.log('3');
})
// 3
// 2
// 1 (3秒之後執行列印的值)
複製程式碼
如上示例解釋:
- 1.先執行
new Promise
第一層的程式碼,遇到setTimeout
,將其推入巨集任務佇列中(此時未執行,排在當前script程式碼塊的巨集任務之後執行),然後遇到了resolve
,執行Promise
後面的程式碼。- 2.遇到
.then 1-1
,推入微任務佇列裡(只是推入,並未執行,所以.then 1-2
的執行時機還沒有到),這個時候發現沒有其他的操作需要處理(比如推其他的微任務到佇列裡),那麼就執行當前微任務佇列裡的函式,也就是執行.then 1-1
的回撥函式。- 3.執行
.then 1-1
的回撥函式的時候,發現了裡面有一個完成態的Promise
物件,不用管繼續走,遇到了.then 2-1
,推入微任務佇列(只是推入,並未執行),此時.then 1-1
回撥執行完畢(沒有return值,相當於return了一個undefined),然後Promise
得以繼續往下執行,遇到了.then 1-2
,繼續推入微任務佇列(依然沒執行),這時發現沒有其他操作,開始順位執行當前微任務佇列裡的函式(此時微任務佇列裡存放了.then 2-1
和.then 1-2
的回撥函式),執行.then 2-1
的回撥函式時,又遇到了一個完成態的Promise
,不用管繼續走,遇到了.then 3-1
,將其推入微任務佇列(未執行),然後執行.then1-2
的回撥,列印3
,此時已經沒有了其他的操作,所以繼續執行微任務佇列裡剩餘的函式,即.then 3-1
的回撥函式,列印2
。- 4.至此,微任務佇列已經執行完畢,開始執行巨集任務佇列中的下一個巨集任務,列印
1
。
再舉個Promise 中微任務順序的栗子2:
var p2 = new Promise( (resolve, reject) => {
setTimeout( () => {
console.log('1');
}, 3000)
resolve(1);
}).then( () => { // 描述:.then() 1-1
Promise.resolve().then( () => { // 描述:.then() 2-1
console.log('2');
}).then( () => { // 描述:.then() 1-2
console.log('3');
})
})
// 2
// 3
// 1 (3秒之後執行列印的值)
複製程式碼
如上示例解釋:
- 1.如同栗子1。
- 2.如同栗子2。
- 3.執行
.then 1-1
的回撥函式的時候,發現了裡面有一個完成態的Promise
物件,不用管繼續走,遇到了.then 2-1
,推入微任務佇列(只是推入,並未執行),此時.then 1-1
回撥執行完畢(沒有return
值,相當於return
了一個undefined
),然後Promise
得以繼續往下執行,遇到了.then 1-2
,繼續推入微任務佇列(依然沒執行),這時發現沒有其他操作,開始順位執行當前微任務佇列裡的函式(此時微任務佇列裡存放了.then 2-1
和.then 1-2
的回撥函式),執行.then 2-1
的回撥函式,列印2
,然後執行.then1-2
的回撥,列印3
。- 4.至此,微任務佇列已經執行完畢,開始執行巨集任務佇列中的下一個巨集任務,列印
1
。
除了提供了例項方法以外,Promise還提供了一些類方法,也就是不用建立例項,也可以呼叫的方法,下面列舉幾個栗子:
Promise.all(iterable)
方法返回一個 Promise
例項,此例項在 iterable
引數內所有的 promise
都“完成(resolved
)”或引數中不包含 promise
時回撥完成(resolve
);如果引數中 promise
有一個失敗(rejected
),此例項回撥失敗(reject)
,失敗原因的是第一個失敗 promise
的結果。
var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 3000, 'foo');
});
Promise.all([promise1, promise2, promise3]).then(values => {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
複製程式碼
解析:
因為例項
promise3
還沒有進入成功fulfilled
狀態;等到了3000
毫秒以後,例項promise3
也進入了成功fulfilled
狀態,Promise.all( )
才會進入then
方法,然後在控制檯輸出:[3, 42, "foo"]
應用場景:我們執行某個操作,這個操作需要得到需要多個介面請求回來的資料來支援,但是這些介面請求之前互不依賴,不需要層層巢狀。這種情況下就適合使用Promise.all( )
方法,因為它會得到所有介面都請求成功了,才會進行操作。
注意:
如果傳入的 promise
中有一個失敗(rejected
),Promise.all
非同步地將失敗的那個結果給失敗狀態的回撥函式,而不管其它 promise
是否完成。
finally
方法用於指定不管 Promise 物件最後狀態如何,都會執行的操作。該方法是 ES2018 引入標準的。這避免了同樣的語句需要在then()
和catch()
中各寫一次的情況。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
複製程式碼
上面程式碼中,不管promise
最後的狀態,在執行完then
或catch
指定的回撥函式以後,都會執行finally
方法指定的回撥函式。
下面是一個例子,伺服器使用 Promise
處理請求,然後使用finally
方法關掉伺服器。
server.listen(port)
.then(function () {
// ...
})
.finally(server.stop);
複製程式碼
如果你想在 promise
執行完畢後無論其結果怎樣都做一些處理或清理時,finally()
方法可能是有用的。
Promise.race()
方法同樣是將多個 Promise
例項,包裝成一個新的 Promise
例項。
const p = Promise.race([p1, p2, p3]);
複製程式碼
上面程式碼中,只要p1
、p2
、p3
之中有一個例項率先改變狀態,p
的狀態就跟著改變。那個率先改變的 Promise
例項的返回值,就傳遞給p的回撥函式。
Promise.race
方法的引數與Promise.all
方法一樣,如果不是 Promise
例項,就會先呼叫下面講到的Promise.resolve
方法,將引數轉為 Promise
例項,再進一步處理。
const p = Promise.race([
fetch('index.php'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(console.log)
.catch(console.error);
複製程式碼
上面程式碼中,如果 5 秒之內fetch
方法無法返回結果,變數p
的狀態就會變為rejected
,從而觸發catch
方法指定的回撥函式。
let promise1 = new Promise(function(resolve){
setTimeout(function () {
resolve('promise1例項成功');
},4000);
});
let promise2 = new Promise(function(resolve,reject){
setTimeout(function () {
reject('promise2例項失敗');
},2000);
});
Promise.race([promise1, promise2]).then(function(result){
console.log(result);
}).catch(function(error){
console.log(error);
});
// expected output: promise2例項失敗
複製程式碼
由於promise2
例項中2000
毫秒之後就執行reject方法,早於例項promise1
的5000
毫秒,所以最後輸出的是:promise2例項失敗。
有時需要將現有物件轉為 Promise 物件,Promise.resolve
方法就起到這個作用。
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
複製程式碼
上面程式碼將 jQuery
生成的deferred
物件,轉為一個新的 Promise
物件。
Promise.resolve
等價於下面的寫法。
Promise.resolve('foo')
// 等價於
new Promise(resolve => resolve('foo'))
複製程式碼
Promise.resolve(value)
方法返回一個以給定值解析後的Promise
物件。如果該值為promise
,返回這個promise
;如果這個值是thenable
(即帶有"then" 方法)),返回的promise會“跟隨”這個thenable的物件,採用它的最終狀態;否則返回的promise
將以此值完成。此函式將類promise
物件的多層巢狀展平。
警告
:不要在解析為自身的thenable 上呼叫Promise.resolve。這將導致無限遞迴,因為它試圖展平無限巢狀的promise。一個例子是將它與Angular中的非同步管道一起使用。
使用靜態Promise.resolve方法
Promise.resolve("Success").then(function(value) {
console.log(value); // "Success"
}, function(value) {
// 不會被呼叫
});
複製程式碼
resolve一個陣列
var p = Promise.resolve([1,2,3]);
p.then(function(v) {
console.log(v[0]); // 1
});
複製程式碼
Resolve另一個promise
var original = Promise.resolve(33);
var cast = Promise.resolve(original);
cast.then(function(value) {
console.log('value: ' + value);
});
console.log('original === cast ? ' + (original === cast));
/*
* 列印順序如下,這裡有一個同步非同步先後執行的區別
* original === cast ? true
* value: 33
*/
複製程式碼
Promise.reject(reason)
方法返回一個帶有拒絕原因reason
引數的Promise
物件。
const p = Promise.reject('出錯了');
// 等同於
const p = new Promise((resolve, reject) => reject('出錯了'))
p.then(null, function (s) {
console.log(s)
});
// 出錯了
複製程式碼
上面程式碼生成一個 Promise
物件的例項 p
,狀態為rejected
,回撥函式會立即執行。
注意
,Promise.reject()
方法的引數,會原封不動地作為reject
的理由,變成後續方法的引數。這一點與Promise.resolve
方法不一致。
const thenable = {
then(resolve, reject) {
reject('出錯了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
複製程式碼
舉一個promise 應用的栗子:
我們可以將圖片的載入寫成一個Promise
,一旦載入完成,Promise
的狀態就發生變化。
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
複製程式碼
Generator 函式與 Promise 的結合
使用 Generator函式 管理流程,遇到非同步操作的時候,通常返回一個Promise物件。
function getFoo () {
return new Promise(function (resolve, reject){
resolve('foo');
});
}
const g = function* () {
try {
const foo = yield getFoo();
console.log(foo);
} catch (e) {
console.log(e);
}
};
function run (generator) {
const it = generator();
function go(result) {
if (result.done) return result.value;
return result.value.then(function (value) {
return go(it.next(value));
}, function (error) {
return go(it.throw(error));
});
}
go(it.next());
}
run(g);
複製程式碼
上面程式碼的 Generator
函式g之中,有一個非同步操作getFoo
,它返回的就是一個Promise
物件。函式run
用來處理這個Promise
物件,並呼叫下一個next
方法。
我目前所寫的專案大多數都是Generator函式 與 Promise 的結合。
這個篇幅有點長,如果你沒有收藏可以收藏,以後慢慢的觀看。
以下是我的公眾號,關注我,會讓你有意想不到的收穫~