在學(xué)習(xí)本章節(jié)內(nèi)容前,你需要先了解什么是異步編程,可以參考:JavaScript 異步編程
Promise 是一個(gè) ECMAScript 6 提供的類(lèi),目的是更加優(yōu)雅地書(shū)寫(xiě)復(fù)雜的異步任務(wù)。
由于 Promise 是 ES6 新增加的,所以一些舊的瀏覽器并不支持,Promise 對(duì)象代表了未來(lái)將要發(fā)生的事件患蹂,用來(lái)傳遞異步操作的消息。
構(gòu)造 Promise
現(xiàn)在我們新建一個(gè) Promise 對(duì)象:
Promise 構(gòu)造函數(shù)包含一個(gè)參數(shù)和一個(gè)帶有 resolve(解析)和 reject(拒絕)兩個(gè)參數(shù)的回調(diào)砸紊。在回調(diào)中執(zhí)行一些操作(例如異步)传于,如果一切都正常,則調(diào)用 resolve醉顽,否則調(diào)用 reject沼溜。
new Promise(function (resolve, reject) {
// 要做的事情...(異步處理)
// 處理結(jié)束后、調(diào)用resolve 或 reject
});
通過(guò)新建一個(gè) Promise 對(duì)象好像并沒(méi)有看出它怎樣 "更加優(yōu)雅地書(shū)寫(xiě)復(fù)雜的異步任務(wù)"游添。我們之前遇到的異步任務(wù)都是一次異步系草,如果需要多次調(diào)用異步函數(shù)呢?例如唆涝,如果我想分三次輸出字符串找都,第一次間隔 1 秒,第二次間隔 4 秒廊酣,第三次間隔 3 秒:
setTimeout(function () {
console.log("First");
setTimeout(function () {
console.log("Second");
setTimeout(function () {
console.log("Third");
}, 3000);
}, 4000);
}, 1000);
//First
//Second
//Third
這段程序?qū)崿F(xiàn)了這個(gè)功能能耻,但是它是用 "函數(shù)瀑布" 來(lái)實(shí)現(xiàn)的。可想而知晓猛,在一個(gè)復(fù)雜的程序當(dāng)中饿幅,用 "函數(shù)瀑布" 實(shí)現(xiàn)的程序無(wú)論是維護(hù)還是異常處理都是一件特別繁瑣的事情,而且會(huì)讓縮進(jìn)格式變得非常冗贅戒职。
現(xiàn)在我們用 Promise 來(lái)實(shí)現(xiàn)同樣的功能:
new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("First");
resolve();
}, 1000);
}).then(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("Second");
resolve();
}, 4000);
});
}).then(function () {
setTimeout(function () {
console.log("Third");
}, 3000);
});
//First
//Second
//Third
這段代碼較長(zhǎng)栗恩,所以還不需要完全理解它,我想引起注意的是 Promise 將嵌套格式的代碼變成了順序格式的代碼洪燥。
Promise 的使用
下面我們通過(guò)剖析這段 Promise "計(jì)時(shí)器" 代碼來(lái)講述 Promise 的使用:
Promise 構(gòu)造函數(shù)只有一個(gè)參數(shù)磕秤,是一個(gè)函數(shù),這個(gè)函數(shù)在構(gòu)造之后會(huì)直接被異步運(yùn)行蚓曼,所以我們稱(chēng)之為起始函數(shù)亲澡。起始函數(shù)包含兩個(gè)參數(shù) resolve 和 reject。
當(dāng) Promise 被構(gòu)造時(shí)纫版,起始函數(shù)會(huì)被異步執(zhí)行:
new Promise(function (resolve, reject) {
console.log("Run");
});
這段程序會(huì)直接輸出 Run。
resolve 和 reject 都是函數(shù)客情,其中調(diào)用 resolve 代表一切正常其弊,reject 是出現(xiàn)異常時(shí)所調(diào)用的:
new Promise(function (resolve, reject) {
var a = 0;
var b = 1;
if (b == 0) reject("Divide zero");
else resolve(a / b);
}).then(function (value) {
console.log("a / b = " + value);
}).catch(function (err) {
console.log(err);
}).finally(function () {
console.log("End");
});
這段程序執(zhí)行結(jié)果是:
a / b = 0
End
Promise 類(lèi)有.then()
.catch()
和.finally()
三個(gè)方法,這三個(gè)方法的參數(shù)都是一個(gè)函數(shù)膀斋,.then() 可以將參數(shù)中的函數(shù)添加到當(dāng)前 Promise 的正常執(zhí)行序列梭伐,.catch() 則是設(shè)定 Promise 的異常處理序列,.finally() 是在 Promise 執(zhí)行的最后一定會(huì)執(zhí)行的序列仰担。 .then() 傳入的函數(shù)會(huì)按順序依次執(zhí)行糊识,有任何異常都會(huì)直接跳到 catch 序列:
new Promise(function (resolve, reject) {
console.log(1111);
resolve(2222); //輸出2222數(shù)據(jù)
}).then(function (value) {
console.log(value); //打印2222數(shù)據(jù)
return 3333; //輸出3333數(shù)據(jù)
}).then(function (value) {
console.log(value); //打印3333數(shù)據(jù)
throw "An error"; //拋出錯(cuò)誤數(shù)據(jù),終止后續(xù) then 操作
}).catch(function (err) {
console.log(err); //打印錯(cuò)誤數(shù)據(jù)
});
執(zhí)行結(jié)果:
1111
2222
3333
An error
resolve() 中可以放置一個(gè)參數(shù)用于向下一個(gè) then 傳遞一個(gè)值,then 中的函數(shù)也可以返回一個(gè)值傳遞給 then摔蓝。但是赂苗,如果 then 中返回的是一個(gè) Promise 對(duì)象,那么下一個(gè) then 將相當(dāng)于對(duì)這個(gè)返回的 Promise 進(jìn)行操作贮尉,這一點(diǎn)從剛才的計(jì)時(shí)器的例子中可以看出來(lái)拌滋。
reject() 參數(shù)中一般會(huì)傳遞一個(gè)異常給之后的 catch 函數(shù)用于處理異常。
比較下then 中返回的是對(duì)象 / 值
then 中返回的是一個(gè) Promise 對(duì)象
如果返回的是Promise 對(duì)象猜谚,第一個(gè)回調(diào)函數(shù)完成以后败砂,會(huì)將返回結(jié)果作為參數(shù),傳入第二個(gè)回調(diào)函數(shù)魏铅。
如果前一個(gè)回調(diào)函數(shù)返回的是Promise對(duì)象昌犹,這時(shí)后一個(gè)回調(diào)函數(shù)就會(huì)等待該P(yáng)romise對(duì)象有了運(yùn)行結(jié)果,才會(huì)進(jìn)一步調(diào)用览芳。(獲取結(jié)果順序不變(相當(dāng)于同步編程))
new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("First");
resolve();
}, 1000);
}).then(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("Second");
resolve();
}, 4000);
});
}).then(function () {
setTimeout(function () {
console.log("Third");
}, 3000);
});
//First
//Second
//Third
then 中返回的是一個(gè)值
如果返回的是值斜姥,獲取結(jié)果順序?qū)⒏鶕?jù) 時(shí)間先后 排列, 而且第二個(gè)then無(wú)法獲取Promise數(shù)據(jù),只能獲取上一個(gè)then中返回的數(shù)據(jù)疾渴,沒(méi)有則返回undefined
new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("First");
resolve("Second")
}, 1000);
}).then(function (value) { //第一個(gè)then
setTimeout(function () {
console.log(value);
}, 3000);
return "Third"
}).then(function (value) { //第二個(gè)then
setTimeout(function () {
console.log(value);
}, 2000);
});
//First
//Third
//Second
但是請(qǐng)注意以下兩點(diǎn):
- resolve 和 reject 的作用域只有起始函數(shù)千贯,不包括 then 以及其他序列;
- resolve 和 reject 并不能夠使起始函數(shù)停止運(yùn)行搞坝,別忘了 return搔谴。
Promise 函數(shù)
上述的 "計(jì)時(shí)器" 程序看上去比函數(shù)瀑布還要長(zhǎng),所以我們可以將它的核心部分寫(xiě)成一個(gè) Promise 函數(shù):
//有setTimeout
function print(delay, message) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(message);
resolve();
}, delay);
});
}
//無(wú)setTimeout
function print( message) {
return new Promise(function (resolve, reject) {
console.log(message);
resolve();
});
}
然后我們就可以放心大膽的實(shí)現(xiàn)程序功能了:
//有setTimeout
print(1000, "First").then(function () {
return print(4000, "Second");
}).then(function () {
print(3000, "Third");
});
//無(wú)setTimeout
print("First").then(function () {
return print("Second");
}).then(function () {
print("Third");
});
這種返回值為一個(gè) Promise 對(duì)象的函數(shù)稱(chēng)作 Promise 函數(shù)桩撮,它常常用于開(kāi)發(fā)基于異步操作的庫(kù)敦第。
回答常見(jiàn)的問(wèn)題(FAQ)
Q: then、catch 和 finally 序列能否順序顛倒店量?
A: 可以芜果,效果完全一樣。但不建議這樣做融师,最好按 then-catch-finally 的順序編寫(xiě)程序右钾。
Q: 除了 then 塊以外,其它兩種塊能否多次使用旱爆?
A: 可以舀射,finally 與 then 一樣會(huì)按順序執(zhí)行,但是 catch 塊只會(huì)執(zhí)行第一個(gè)怀伦,除非 catch 塊里有異常脆烟。所以最好只安排一個(gè) catch 和 finally 塊。
Q: then 塊如何中斷房待?
A: then 塊默認(rèn)會(huì)向下順序執(zhí)行邢羔,return 是不能中斷的,可以通過(guò) throw 來(lái)跳轉(zhuǎn)至 catch 實(shí)現(xiàn)中斷桑孩。
Q: 什么時(shí)候適合用 Promise 而不是傳統(tǒng)回調(diào)函數(shù)拜鹤?
A: 當(dāng)需要多次順序執(zhí)行異步操作的時(shí)候,例如洼怔,如果想通過(guò)異步方法先后檢測(cè)用戶(hù)名和密碼署惯,需要先異步檢測(cè)用戶(hù)名,然后再異步檢測(cè)密碼的情況下就很適合 Promise镣隶。
Q: Promise 是一種將異步轉(zhuǎn)換為同步的方法嗎极谊?
A: 完全不是。Promise 只不過(guò)是一種更良好的編程風(fēng)格安岂。
Q: 什么時(shí)候我們需要再寫(xiě)一個(gè) then 而不是在當(dāng)前的 then 接著編程轻猖?
A: 當(dāng)你又需要調(diào)用一個(gè)異步任務(wù)的時(shí)候。
異步函數(shù)
異步函數(shù)(async function)是 ECMAScript 2017 (ECMA-262) 標(biāo)準(zhǔn)的規(guī)范域那,幾乎被所有瀏覽器所支持咙边,除了 Internet Explorer猜煮。
在 Promise 中我們編寫(xiě)過(guò)一個(gè) Promise 函數(shù):
function print(delay, message) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(message);
resolve();
}, delay);
});
}
然后用不同的時(shí)間間隔輸出了三行文本:
print(1000, "First").then(function () {
return print(4000, "Second");
}).then(function () {
print(3000, "Third");
});
我們可以將這段代碼變得更好看:async / await
async function asyncFunc() {
await print(1000, "First");
await print(4000, "Second");
await print(3000, "Third");
}
asyncFunc();
//First
//Second
//Third
哈!這豈不是將異步操作變得像同步操作一樣容易了嗎败许!
這次的回答是肯定的王带,異步函數(shù) async function 中可以使用 await 指令,await 指令后必須跟著一個(gè) Promise市殷,異步函數(shù)會(huì)在這個(gè) Promise 運(yùn)行中暫停愕撰,直到其運(yùn)行結(jié)束再繼續(xù)運(yùn)行。
異步函數(shù)實(shí)際上原理與 Promise 原生 API 的機(jī)制是一模一樣的醋寝,只不過(guò)更便于程序員閱讀搞挣。
處理異常的機(jī)制將用 try-catch 塊實(shí)現(xiàn):
async function asyncFunc() {
try{
await print(1000, "First");
await print(4000, "Second");
await print(3000, "Third");
throw "error";
}catch(err){
console.log(err)
}
}
asyncFunc();
//First
//Second
//error
如果 Promise 有一個(gè)正常的返回值,await 語(yǔ)句也會(huì)返回它:
async function asyncFunc() {
let value = await new Promise(
function (resolve, reject) {
resolve("Return value");
}
);
console.log(value);
}
asyncFunc();
程序會(huì)輸出:
Return value
Promise 對(duì)象有以下兩個(gè)特點(diǎn):
1音羞、對(duì)象的狀態(tài)不受外界影響囱桨。Promise 對(duì)象代表一個(gè)異步操作,有三種狀態(tài):
pending: 初始狀態(tài)嗅绰,不是成功或失敗狀態(tài)舍肠。
fulfilled: 意味著操作成功完成。
rejected: 意味著操作失敗窘面。
只有異步操作的結(jié)果貌夕,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都無(wú)法改變這個(gè)狀態(tài)民镜。這也是 Promise 這個(gè)名字的由來(lái),它的英語(yǔ)意思就是「承諾」险毁,表示其他手段無(wú)法改變制圈。
2、一旦狀態(tài)改變畔况,就不會(huì)再變鲸鹦,任何時(shí)候都可以得到這個(gè)結(jié)果。Promise 對(duì)象的狀態(tài)改變跷跪,只有兩種可能:從 Pending 變?yōu)?Resolved 和從 Pending 變?yōu)?Rejected馋嗜。只要這兩種情況發(fā)生,狀態(tài)就凝固了吵瞻,不會(huì)再變了葛菇,會(huì)一直保持這個(gè)結(jié)果。就算改變已經(jīng)發(fā)生了橡羞,你再對(duì) Promise 對(duì)象添加回調(diào)函數(shù)眯停,也會(huì)立即得到這個(gè)結(jié)果。這與事件(Event)完全不同卿泽,事件的特點(diǎn)是莺债,如果你錯(cuò)過(guò)了它,再去監(jiān)聽(tīng),是得不到結(jié)果的齐邦。
Promise 優(yōu)缺點(diǎn)
有了 Promise 對(duì)象椎侠,就可以將異步操作以同步操作的流程表達(dá)出來(lái),避免了層層嵌套的回調(diào)函數(shù)措拇。此外我纪,Promise 對(duì)象提供統(tǒng)一的接口,使得控制異步操作更加容易儡羔。
Promise 也有一些缺點(diǎn)宣羊。首先,無(wú)法取消 Promise汰蜘,一旦新建它就會(huì)立即執(zhí)行仇冯,無(wú)法中途取消。其次族操,如果不設(shè)置回調(diào)函數(shù)苛坚,Promise 內(nèi)部拋出的錯(cuò)誤,不會(huì)反應(yīng)到外部色难。第三泼舱,當(dāng)處于 Pending 狀態(tài)時(shí),無(wú)法得知目前進(jìn)展到哪一個(gè)階段(剛剛開(kāi)始還是即將完成)枷莉。
【筆記】
Promise 對(duì)象代表一個(gè)異步操作娇昙,有三種狀態(tài):Pending(進(jìn)行中)、Resolved(已完成笤妙,又稱(chēng) Fulfilled)和 Rejected(已失斆罢啤)。
通過(guò)回調(diào)里的 resolve(data) 將這個(gè) promise 標(biāo)記為 resolverd蹲盘,然后進(jìn)行下一步 then((data)=>{//do something})股毫,resolve 里的參數(shù)就是你要傳入 then 的數(shù)據(jù)。