Promise 對象是 JavaScript 的異步操作解決方案,為異步操作提供統(tǒng)一接口。它起到代理作用(proxy),充當異步操作與回調(diào)函數(shù)之間的中介,使得異步操作具備同步操作的接口。Promise 可以讓異步操作寫起來,就像在寫同步操作的流程,而不必一層層地嵌套回調(diào)函數(shù)。
注意,本章只是 Promise 對象的簡單介紹。為了避免與后續(xù)教程的重復,更完整的介紹請看《ES6 標準入門》的《Promise 對象》一章。
首先,Promise 是一個對象,也是一個構(gòu)造函數(shù)。
function f1(resolve, reject) {
// 異步代碼...
}
var p1 = new Promise(f1);
上面代碼中,Promise
構(gòu)造函數(shù)接受一個回調(diào)函數(shù)f1
作為參數(shù),f1
里面是異步操作的代碼。然后,返回的p1
就是一個 Promise 實例。
Promise 的設計思想是,所有異步任務都返回一個 Promise 實例。Promise 實例有一個then
方法,用來指定下一步的回調(diào)函數(shù)。
var p1 = new Promise(f1);
p1.then(f2);
上面代碼中,f1
的異步操作執(zhí)行完成,就會執(zhí)行f2
。
傳統(tǒng)的寫法可能需要把f2
作為回調(diào)函數(shù)傳入f1
,比如寫成f1(f2)
,異步操作完成后,在f1
內(nèi)部調(diào)用f2
。Promise 使得f1
和f2
變成了鏈式寫法。不僅改善了可讀性,而且對于多層嵌套的回調(diào)函數(shù)尤其方便。
// 傳統(tǒng)寫法
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// ...
});
});
});
});
// Promise 的寫法
(new Promise(step1))
.then(step2)
.then(step3)
.then(step4);
從上面代碼可以看到,采用 Promises 以后,程序流程變得非常清楚,十分易讀。注意,為了便于理解,上面代碼的Promise
實例的生成格式,做了簡化,真正的語法請參照下文。
總的來說,傳統(tǒng)的回調(diào)函數(shù)寫法使得代碼混成一團,變得橫向發(fā)展而不是向下發(fā)展。Promise 就是解決這個問題,使得異步流程可以寫成同步流程。
Promise 原本只是社區(qū)提出的一個構(gòu)想,一些函數(shù)庫率先實現(xiàn)了這個功能。ECMAScript 6 將其寫入語言標準,目前 JavaScript 原生支持 Promise 對象。
Promise 對象通過自身的狀態(tài),來控制異步操作。Promise 實例具有三種狀態(tài)。
上面三種狀態(tài)里面,fulfilled
和rejected
合在一起稱為resolved
(已定型)。
這三種的狀態(tài)的變化途徑只有兩種。
一旦狀態(tài)發(fā)生變化,就凝固了,不會再有新的狀態(tài)變化。這也是 Promise 這個名字的由來,它的英語意思是“承諾”,一旦承諾成效,就不得再改變了。這也意味著,Promise 實例的狀態(tài)變化只可能發(fā)生一次。
因此,Promise 的最終結(jié)果只有兩種。
fulfilled
?。rejected
?。JavaScript 提供原生的Promise
構(gòu)造函數(shù),用來生成 Promise 實例。
var promise = new Promise(function (resolve, reject) {
// ...
if (/* 異步操作成功 */){
resolve(value);
} else { /* 異步操作失敗 */
reject(new Error());
}
});
上面代碼中,Promise
構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別是resolve
和reject
。它們是兩個函數(shù),由 JavaScript 引擎提供,不用自己實現(xiàn)。
resolve
函數(shù)的作用是,將Promise
實例的狀態(tài)從“未完成”變?yōu)椤俺晒Α保磸?code>pending變?yōu)?code>fulfilled),在異步操作成功時調(diào)用,并將異步操作的結(jié)果,作為參數(shù)傳遞出去。reject
函數(shù)的作用是,將Promise
實例的狀態(tài)從“未完成”變?yōu)椤笆 保磸?code>pending變?yōu)?code>rejected),在異步操作失敗時調(diào)用,并將異步操作報出的錯誤,作為參數(shù)傳遞出去。
下面是一個例子。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100)
上面代碼中,timeout(100)
返回一個 Promise 實例。100毫秒以后,該實例的狀態(tài)會變?yōu)?code>fulfilled。
Promise 實例的then
方法,用來添加回調(diào)函數(shù)。
then
方法可以接受兩個回調(diào)函數(shù),第一個是異步操作成功時(變?yōu)?code>fulfilled狀態(tài))的回調(diào)函數(shù),第二個是異步操作失?。ㄗ?yōu)?code>rejected)時的回調(diào)函數(shù)(該參數(shù)可以省略)。一旦狀態(tài)改變,就調(diào)用相應的回調(diào)函數(shù)。
var p1 = new Promise(function (resolve, reject) {
resolve('成功');
});
p1.then(console.log, console.error);
// "成功"
var p2 = new Promise(function (resolve, reject) {
reject(new Error('失敗'));
});
p2.then(console.log, console.error);
// Error: 失敗
上面代碼中,p1
和p2
都是Promise 實例,它們的then
方法綁定兩個回調(diào)函數(shù):成功時的回調(diào)函數(shù)console.log
,失敗時的回調(diào)函數(shù)console.error
(可以省略)。p1
的狀態(tài)變?yōu)槌晒Γ?code>p2的狀態(tài)變?yōu)槭?,對應的回調(diào)函數(shù)會收到異步操作傳回的值,然后在控制臺輸出。
then
方法可以鏈式使用。
p1
.then(step1)
.then(step2)
.then(step3)
.then(
console.log,
console.error
);
上面代碼中,p1
后面有四個then
,意味依次有四個回調(diào)函數(shù)。只要前一步的狀態(tài)變?yōu)?code>fulfilled,就會依次執(zhí)行緊跟在后面的回調(diào)函數(shù)。
最后一個then
方法,回調(diào)函數(shù)是console.log
和console.error
,用法上有一點重要的區(qū)別。console.log
只顯示step3
的返回值,而console.error
可以顯示p1
、step1
、step2
、step3
之中任意一個發(fā)生的錯誤。舉例來說,如果step1
的狀態(tài)變?yōu)?code>rejected,那么step2
和step3
都不會執(zhí)行了(因為它們是resolved
的回調(diào)函數(shù))。Promise 開始尋找,接下來第一個為rejected
的回調(diào)函數(shù),在上面代碼中是console.error
。這就是說,Promise 對象的報錯具有傳遞性。
Promise 的用法,簡單說就是一句話:使用then
方法添加回調(diào)函數(shù)。但是,不同的寫法有一些細微的差別,請看下面四種寫法,它們的差別在哪里?
// 寫法一
f1().then(function () {
return f2();
});
// 寫法二
f1().then(function () {
f2();
});
// 寫法三
f1().then(f2());
// 寫法四
f1().then(f2);
為了便于講解,下面這四種寫法都再用then
方法接一個回調(diào)函數(shù)f3
。寫法一的f3
回調(diào)函數(shù)的參數(shù),是f2
函數(shù)的運行結(jié)果。
f1().then(function () {
return f2();
}).then(f3);
寫法二的f3
回調(diào)函數(shù)的參數(shù)是undefined
。
f1().then(function () {
f2();
return;
}).then(f3);
寫法三的f3
回調(diào)函數(shù)的參數(shù),是f2
函數(shù)返回的函數(shù)的運行結(jié)果。
f1().then(f2())
.then(f3);
寫法四與寫法一只有一個差別,那就是f2
會接收到f1()
返回的結(jié)果。
f1().then(f2)
.then(f3);
下面是使用 Promise 完成圖片的加載。
var preloadImage = function (path) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
上面代碼中,image
是一個圖片對象的實例。它有兩個事件監(jiān)聽屬性,onload
屬性在圖片加載成功后調(diào)用,onerror
屬性在加載失敗調(diào)用。
上面的preloadImage()
函數(shù)用法如下。
preloadImage('https://example.com/my.jpg')
.then(function (e) { document.body.append(e.target) })
.then(function () { console.log('加載成功') })
上面代碼中,圖片加載成功以后,onload
屬性會返回一個事件對象,因此第一個then()
方法的回調(diào)函數(shù),會接收到這個事件對象。該對象的target
屬性就是圖片加載后生成的 DOM 節(jié)點。
Promise 的優(yōu)點在于,讓回調(diào)函數(shù)變成了規(guī)范的鏈式寫法,程序流程可以看得很清楚。它有一整套接口,可以實現(xiàn)許多強大的功能,比如同時執(zhí)行多個異步操作,等到它們的狀態(tài)都改變以后,再執(zhí)行一個回調(diào)函數(shù);再比如,為多個回調(diào)函數(shù)中拋出的錯誤,統(tǒng)一指定處理方法等等。
而且,Promise 還有一個傳統(tǒng)寫法沒有的好處:它的狀態(tài)一旦改變,無論何時查詢,都能得到這個狀態(tài)。這意味著,無論何時為 Promise 實例添加回調(diào)函數(shù),該函數(shù)都能正確執(zhí)行。所以,你不用擔心是否錯過了某個事件或信號。如果是傳統(tǒng)寫法,通過監(jiān)聽事件來執(zhí)行回調(diào)函數(shù),一旦錯過了事件,再添加回調(diào)函數(shù)是不會執(zhí)行的。
Promise 的缺點是,編寫的難度比傳統(tǒng)寫法高,而且閱讀代碼也不是一眼可以看懂。你只會看到一堆then
,必須自己在then
的回調(diào)函數(shù)里面理清邏輯。
Promise 的回調(diào)函數(shù)屬于異步任務,會在同步任務之后執(zhí)行。
new Promise(function (resolve, reject) {
resolve(1);
}).then(console.log);
console.log(2);
// 2
// 1
上面代碼會先輸出2,再輸出1。因為console.log(2)
是同步任務,而then
的回調(diào)函數(shù)屬于異步任務,一定晚于同步任務執(zhí)行。
但是,Promise 的回調(diào)函數(shù)不是正常的異步任務,而是微任務(microtask)。它們的區(qū)別在于,正常任務追加到下一輪事件循環(huán),微任務追加到本輪事件循環(huán)。這意味著,微任務的執(zhí)行時間一定早于正常任務。
setTimeout(function() {
console.log(1);
}, 0);
new Promise(function (resolve, reject) {
resolve(2);
}).then(console.log);
console.log(3);
// 3
// 2
// 1
上面代碼的輸出結(jié)果是321
。這說明then
的回調(diào)函數(shù)的執(zhí)行時間,早于setTimeout(fn, 0)
。因為then
是本輪事件循環(huán)執(zhí)行,setTimeout(fn, 0)
在下一輪事件循環(huán)開始時執(zhí)行。
更多建議: