前言:
日常開發(fā)中,異步操作幾乎每天都能見到。傳統(tǒng)的意不解決方案是通過回調(diào)函數(shù),隨著程序邏輯越來越復(fù)雜,回調(diào)函數(shù)的方式變得越來越繁瑣,很容易出現(xiàn)回調(diào)地獄,于是一種更合理更強(qiáng)大的代替方案出現(xiàn)--Promise,接下來就深入學(xué)習(xí)Promise是如何解決異步操作的。
一.基礎(chǔ)
定義: Promise 對象用于表示一個異步操作的最終完成 (或失敗), 及其結(jié)果值.
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('foo');
}, 300);
});
promise1.then((value) => {
console.log(value);
// expected output: "foo"
});
console.log(promise1);
// expected output: [object Promise]
運(yùn)行上面這段代碼,先是打印 [object Promise] ,300ms后打印foo
語法
new Promise( function(resolve, reject) {...} /* executor */ )
參數(shù) executor executor是帶有 resolve 和 reject 兩個參數(shù)的函數(shù) 。Promise構(gòu)造函數(shù)執(zhí)行時立即調(diào)用executor 函數(shù), resolve 和 reject 兩個函數(shù)作為參數(shù)傳遞給executor(executor 函數(shù)在Promise構(gòu)造函數(shù)返回promise實(shí)例對象前被調(diào)用)。resolve 和 reject 函數(shù)被調(diào)用時,分別將promise的狀態(tài)改為fulfilled(完成)或rejected(失敗)。executor 內(nèi)部通常會執(zhí)行一些異步操作,一旦異步操作執(zhí)行完畢(可能成功/失敗),要么調(diào)用resolve函數(shù)來將promise狀態(tài)改成fulfilled,要么調(diào)用reject 函數(shù)將promise的狀態(tài)改為rejected。如果在executor函數(shù)中拋出一個錯誤,那么該promise 狀態(tài)為rejected。executor函數(shù)的返回值被忽略。
這段描述分解下就是:
- 實(shí)例化Promise對象時需要傳入一個executor函數(shù),所有業(yè)務(wù)代碼都需要寫在這個函數(shù)中;
- executor函數(shù)在構(gòu)造函數(shù)執(zhí)行時就會調(diào)用,此時實(shí)例化對象還并沒有被創(chuàng)建, resolve 和 reject 兩個函數(shù)作為參數(shù)傳遞給executor,resolve 和 reject 函數(shù)被調(diào)用時,分別將promise的狀態(tài)改為fulfilled(完成)或rejected(失敗)。一旦狀態(tài)改變,就不會再變,任何時候都可以得到這個結(jié)果。
- 如果executor中代碼拋出了錯誤,promise 狀態(tài)為rejected;
- executor函數(shù)的返回值被忽略。
狀態(tài) 一個 Promise有以下幾種狀態(tài):
- pending: 初始狀態(tài),既不是成功,也不是失敗狀態(tài)。
- fulfilled: 意味著操作成功完成。
- rejected: 意味著操作失敗。
pending 狀態(tài)的 Promise 對象可能會變?yōu)閒ulfilled 狀態(tài)并傳遞一個值給相應(yīng)的狀態(tài)處理方法,也可能變?yōu)槭顟B(tài)(rejected)并傳遞失敗信息。當(dāng)其中任一種情況出現(xiàn)時,Promise 對象的 then 方法綁定的處理方法(handlers )就會被調(diào)用(then方法包含兩個參數(shù):onfulfilled 和 onrejected,它們都是 Function 類型。當(dāng)Promise狀態(tài)為fulfilled時,調(diào)用 then 的 onfulfilled 方法,當(dāng)Promise狀態(tài)為rejected時,調(diào)用 then 的 onrejected 方法, 所以在異步操作的完成和綁定處理方法之間不存在競爭)。
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('fulfilled');
}, 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('rejected');
}, 3000);
});
promise1.then((fulfilled) => {
console.log(fulfilled);
},(rejected)=>{
console.log(rejected);
});
promise2.then((fulfilled) => {
console.log(fulfilled);
},(rejected)=>{
console.log(rejected);
});
運(yùn)行上面這段代碼,1s后打印fulfilled,3s后打印rejected rejected狀態(tài)的 Promise也可以通過.catch進(jìn)行捕獲,因?yàn)?Promise.prototype.then 和 Promise.prototype.catch 方法返回promise 對象, 所以它們可以被鏈?zhǔn)秸{(diào)用。所以上述代碼可以改為:
promise1.then((fulfilled) => {
console.log(fulfilled);
}).catch((rejected)=>{
console.log(rejected);
});
promise2.then((fulfilled) => {
console.log(fulfilled);
}).catch((rejected)=>{
console.log(rejected);
});
二.深入理解
1.Promise 是用來管理異步編程的,它本身不是異步的,new Promise的時候會立即把executor函數(shù)執(zhí)行,只不過我們一般會在executor函數(shù)中處理一個異步操作。例如下面一段代碼:
let firstPromise = new Promise(()=>{
setTimeout(()=>{
console.log(1)
},1000)
console.log(2)
})
console.log(3) // 2 3 1
2.Promise 采用了回調(diào)函數(shù)延遲綁定技術(shù),在執(zhí)行 resolve 函數(shù)的時候,回調(diào)函數(shù)還沒有綁定,那么只能推遲回調(diào)函數(shù)的執(zhí)行。這具體是啥意思呢?我們先來看下面的例子:
let p1 = new Promise((resolve,reject)=>{
console.log(1);
resolve('浪里行舟')
console.log(2)
})
// then:設(shè)置成功或者失敗后處理的方法
p1.then(result=>{
//p1延遲綁定回調(diào)函數(shù)
console.log('成功 '+result)
},reason=>{
console.log('失敗 '+reason)
})
console.log(3)
// 1
// 2
// 3
// 成功 浪里行舟
new Promise的時候先執(zhí)行executor函數(shù),打印出 1、2,Promise在執(zhí)行resolve時,觸發(fā)微任務(wù),還是繼續(xù)往下執(zhí)行同步任務(wù), 執(zhí)行p1.then時,存儲起來兩個函數(shù)(此時這兩個函數(shù)還沒有執(zhí)行),然后打印出3,此時同步任務(wù)執(zhí)行完成,最后執(zhí)行剛剛那個微任務(wù),從而執(zhí)行.then中成功的方法。
3.錯誤處理,多個Promise鏈?zhǔn)讲僮鞯腻e誤捕獲可以通過一個catch處理;例如下面一段代碼:
let executor = function(resolve,reject){
let random = Math.random()
if(random>0.5){
resolve()
}else{
reject()
}
}
let p1 = new Promise(executor)
p1.then(resualt=>{
console.log(1)
return new Promise(executor)
}).then(resualt=>{
console.log(2)
return new Promise(executor)
}).then(resualt=>{
console.log(3)
return new Promise(executor)
}).catch((error) => {
console.log('error', error)
})
這段代碼有四個 Promise 對象,無論哪個對象里面拋出異常,都可以通過最后一個.catch 來捕獲異常,通過這種方式可以將所有 Promise 對象的錯誤合并到一個函數(shù)來處理,這樣就解決了每個任務(wù)都需要單獨(dú)處理異常的問題。
4.常用方法
- Promise.resolve() Promise.resolve(value)方法返回一個以給定值解析后的Promise 對象。 Promise.resolve()等價于下面的寫法:
Promise.resolve('foo')
// 等價于
new Promise(resolve => resolve('foo'))
Promise.resolve方法的參數(shù)分成四種情況。
(1)參數(shù)是一個 Promise 實(shí)例
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
上面代碼中,p1是一個 Promise,3 秒之后變?yōu)閞ejected。p2的狀態(tài)在 1 秒之后改變,resolve方法返回的是p1。由于p2返回的是另一個 Promise,導(dǎo)致p2自己的狀態(tài)無效了,由p1的狀態(tài)決定p2的狀態(tài)。所以,后面的then語句都變成針對后者(p1)。又過了 2 秒,p1變?yōu)閞ejected,導(dǎo)致觸發(fā)catch方法指定的回調(diào)函數(shù)。
(2)參數(shù)不是具有then方法的對象,或根本就不是對象
Promise.resolve("Success").then(function(value) {
// Promise.resolve方法的參數(shù),會同時傳給回調(diào)函數(shù)。
console.log(value); // "Success"
}, function(value) {
// 不會被調(diào)用
});
(3)不帶有任何參數(shù) Promise.resolve()方法允許調(diào)用時不帶參數(shù),直接返回一個resolved狀態(tài)的 Promise 對象。如果希望得到一個 Promise 對象,比較方便的方法就是直接調(diào)用Promise.resolve()方法。
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one two
(4)參數(shù)是一個thenable對象 thenable對象指的是具有then方法的對象,Promise.resolve方法會將這個對象轉(zhuǎn)為 Promise 對象,然后就立即執(zhí)行thenable對象的then方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
- Promise.reject() Promise.reject()方法返回一個帶有拒絕原因的Promise對象。
new Promise((resolve,reject) => {
reject(new Error("出錯了"));
});
// 等價于
Promise.reject(new Error("出錯了"));
// 使用方法
Promise.reject(new Error("BOOM!")).catch(error => {
console.error(error);
});
值得注意的是,調(diào)用resolve或reject以后,Promise 的使命就完成了,后繼操作應(yīng)該放到then方法里面,而不應(yīng)該直接寫在resolve或reject的后面。所以,最好在它們前面加上return語句,這樣就不會有意外。
new Promise((resolve, reject) => {
return reject(1);
// 后面的語句不會執(zhí)行
console.log(2);
})
- Promise.all()
var p1 = Promise.resolve(1)
var p2 = Promise.resolve({a:2})
var p3 = new Promise(function(resolve,reject){
setTimeout(function(){
resolve(3)
},3000)
})
Promise.all([p1,p2,p3]).then(result=>{
// 返回的結(jié)果是按照Array中編寫實(shí)例的順序來
console.log(result)
})
Promise.all 生成并返回一個新的 Promise 對象,所以它可以使用 Promise 實(shí)例的所有方法。參數(shù)傳遞promise數(shù)組中所有的 Promise 對象都變?yōu)閞esolve的時候,該方法才會返回, 新創(chuàng)建的 Promise 則會使用這些 promise 的值。 如果參數(shù)中的任何一個promise為reject的話,則整個Promise.all調(diào)用會立即終止,并返回一個reject的新的 Promise 對象。
- Promise.allSettled()
有時候,我們不關(guān)心異步操作的結(jié)果,只關(guān)心這些操作有沒有結(jié)束。這時,ES2020 引入Promise.allSettled()方法就很有用。如果沒有這個方法,想要確保所有操作都結(jié)束,就很麻煩。Promise.all()方法無法做到這一點(diǎn)。
假如有這樣的場景:一個頁面有三個區(qū)域,分別對應(yīng)三個獨(dú)立的接口數(shù)據(jù),使用 Promise.all 來并發(fā)請求三個接口,如果其中任意一個接口出現(xiàn)異常,狀態(tài)是reject,這會導(dǎo)致頁面中該三個區(qū)域數(shù)據(jù)全都無法出來,顯然這種狀況我們是無法接受,Promise.allSettled的出現(xiàn)就可以解決這個痛點(diǎn):
Promise.allSettled([
Promise.reject({ code: 500, msg: '服務(wù)異常' }),
Promise.resolve({ code: 200, list: [] }),
Promise.resolve({ code: 200, list: [] })
]).then(res => {
console.log(res)
/*
0: {status: "rejected", reason: {…}}
1: {status: "fulfilled", value: {…}}
2: {status: "fulfilled", value: {…}}
*/
// 過濾掉 rejected 狀態(tài),盡可能多的保證頁面區(qū)域數(shù)據(jù)渲染
RenderContent(
res.filter(el => {
return el.status !== 'rejected'
})
)
})
Promise.allSettled跟Promise.all類似, 其參數(shù)接受一個Promise的數(shù)組, 返回一個新的Promise, 唯一的不同在于, 它不會進(jìn)行短路, 也就是說當(dāng)Promise全部處理完成后,我們可以拿到每個Promise的狀態(tài), 而不管是否處理成功。
- Promise.race()
Promise.all()方法的效果是"誰跑的慢,以誰為準(zhǔn)執(zhí)行回調(diào)",那么相對的就有另一個方法"誰跑的快,以誰為準(zhǔn)執(zhí)行回調(diào)",這就是Promise.race()方法,這個詞本來就是賽跑的意思。race的用法與all一樣,接收一個promise對象數(shù)組為參數(shù)。
Promise.all在接收到的所有的對象promise都變?yōu)镕ulFilled或者Rejected狀態(tài)之后才會繼續(xù)進(jìn)行后面的處理,與之相對的是Promise.race只要有一個promise對象進(jìn)入FulFilled或者Rejected狀態(tài)的話,就會繼續(xù)進(jìn)行后面的處理。
// `delay`毫秒后執(zhí)行resolve
function timerPromisefy(delay) {
return new Promise(resolve => {
setTimeout(() => {
resolve(delay);
}, delay);
});
}
// 任何一個promise變?yōu)閞esolve或reject的話程序就停止運(yùn)行
Promise.race([
timerPromisefy(1),
timerPromisefy(32),
timerPromisefy(64)
]).then(function (value) {
console.log(value); // => 1
});
上面的代碼創(chuàng)建了3個promise對象,這些promise對象會分別在1ms、32ms 和 64ms后變?yōu)榇_定狀態(tài),即FulFilled,并且在第一個變?yōu)榇_定狀態(tài)的1ms后,.then注冊的回調(diào)函數(shù)就會被調(diào)用。
- Promise.prototype.finally()
ES9 新增 finally() 方法返回一個Promise。在promise結(jié)束時,無論結(jié)果是fulfilled或者是rejected,都會執(zhí)行指定的回調(diào)函數(shù)。這為在Promise是否成功完成后都需要執(zhí)行的代碼提供了一種方式。這避免了同樣的語句需要在then()和catch()中各寫一次的情況。 比如我們發(fā)送請求之前會出現(xiàn)一個loading,當(dāng)我們請求發(fā)送完成之后,不管請求有沒有出錯,我們都希望關(guān)掉這個loading。
this.loading = true
request()
.then((res) => {
// do something
})
.catch(() => {
// log err
})
.finally(() => {
this.loading = false
})
finally方法的回調(diào)函數(shù)不接受任何參數(shù),這表明,finally方法里面的操作,應(yīng)該是與狀態(tài)無關(guān)的,不依賴于 Promise 的執(zhí)行結(jié)果。
三.實(shí)際應(yīng)用
假設(shè)有這樣一個需求:紅燈 3s 亮一次,綠燈 1s 亮一次,黃燈 2s 亮一次;如何讓三個燈不斷交替重復(fù)亮燈? 三個亮燈函數(shù)已經(jīng)存在:
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
這道題復(fù)雜的地方在于需要“交替重復(fù)”亮燈,而不是亮完一遍就結(jié)束,我們可以通過遞歸來實(shí)現(xiàn):
// 用 promise 實(shí)現(xiàn)
let task = (timer, light) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
}
if (light === 'green') {
green()
}
if (light === 'yellow') {
yellow()
}
resolve()
}, timer);
})
}
let step = () => {
task(3000, 'red')
.then(() => task(1000, 'green'))
.then(() => task(2000, 'yellow'))
.then(step)
}
step()
同樣也可以通過async/await 的實(shí)現(xiàn):
// async/await 實(shí)現(xiàn)
let step = async () => {
await task(3000, 'red')
await task(1000, 'green')
await task(2000, 'yellow')
step()
}
step()
參考資料 你真的懂Promise嗎