
我認(rèn)為 Promise 應(yīng)該算是 ES6 標(biāo)準(zhǔn)最大的亮點(diǎn),它提供了異步編程的一種解決方案。比傳統(tǒng)的回調(diào)函數(shù)和事件解決方案,它更合理、更強(qiáng)大。
一、簡(jiǎn)介
Promise 是一個(gè)容器,里面保存著某個(gè)未來(lái)才會(huì)結(jié)束的事件(一般為異步操作)的結(jié)果。從語(yǔ)法上來(lái)說(shuō),Promise 是一個(gè)對(duì)象,它可以獲取異步操作的消息。
Promise 對(duì)象的特點(diǎn):
Promise對(duì)象有且只有三種狀態(tài):pending、fulfilled、rejected,分別表示進(jìn)行中、已成功、已失敗。一旦狀態(tài)發(fā)生改變,就不會(huì)再變。狀態(tài)的改變只有兩種可能:
pending -> fulfilled或pending -> rejected。若發(fā)生了其中一種情況,狀態(tài)就會(huì)一直保存這個(gè)結(jié)果,這時(shí)就成為resolved(已定型)。
這種以“同步的方式”去表達(dá)異步流程,可以避免層層嵌套的回調(diào)函數(shù),避免出現(xiàn)“回調(diào)地獄”(Callback Hell)。
BTW,網(wǎng)上有些文章把
fulfilled狀態(tài),叫成resolved,盡管我們可能知道他想表達(dá)的意思,但其實(shí)是不對(duì)的。
Promise 對(duì)象的缺點(diǎn):
一是無(wú)法取消 Promise,一旦創(chuàng)建它就會(huì)立即執(zhí)行,無(wú)法中途取消;二是若不設(shè)置回調(diào)函數(shù)情況下,Promise 內(nèi)部拋出錯(cuò)誤,不會(huì)反饋到外部;三是當(dāng)處于 pending 狀態(tài),無(wú)法得知目前進(jìn)展到哪個(gè)階段。
二、Promise 用法
根據(jù)規(guī)定,Promise 是一個(gè)構(gòu)造函數(shù),用來(lái)生成 Promise 實(shí)例對(duì)象。
1. 創(chuàng)建 Promise 對(duì)象
示例:
const handler = (resolve, reject) => {
// some statements...
// 根據(jù)異步操作的結(jié)果,通過(guò) resolve 或 reject 函數(shù)去改變 Promise 對(duì)象的狀態(tài)
if (true) {
// pending -> fulfilled
resolve(...)
} else {
// pending -> rejected
reject(...)
}
// 需要注意的是:
// 1. 在上面 Promise 狀態(tài)已經(jīng)定型(fulfilled 或 rejected),
// 因此,我們?cè)偈褂?resolve() 或 reject() 或主動(dòng)/被動(dòng)拋出錯(cuò)誤的方式,
// 試圖再次修改狀態(tài),是沒(méi)用的,狀態(tài)不會(huì)再發(fā)生改變。
// 2. 當(dāng) Promise 對(duì)象的狀態(tài)“已定型”后,若未使用 return 終止代碼往下執(zhí)行,
// 后面代碼出現(xiàn)的錯(cuò)誤(主動(dòng)拋出或語(yǔ)法錯(cuò)誤等),在外部都不可見,無(wú)法捕獲到。
// 3. hander 函數(shù)的返回值是沒(méi)意義的。怎么理解?
// 假設(shè)內(nèi)部不包括 resolve() 或 reject() 或內(nèi)部不出現(xiàn)語(yǔ)法錯(cuò)誤,
// 或不主動(dòng)拋出錯(cuò)誤,僅有類似 `return 'anything'` 語(yǔ)句,
// 那么 promise 對(duì)象永遠(yuǎn)都是 pending 狀態(tài)。
}
const promise = new Promise(handler)
Promie 構(gòu)造函數(shù)接受一個(gè)函數(shù)作為參數(shù),該函數(shù)的兩個(gè)參數(shù)分別是 resolve 和 reject。而 resolve 和 rejeact 也是函數(shù),其作用是改變 Promise 對(duì)象的狀態(tài),分別是 pending -> fulfilled 和 pending -> rejected。
假設(shè)構(gòu)造函數(shù)內(nèi)不指定 resolve 或 reject 函數(shù),那么 Promise 的對(duì)象會(huì)一直保持著 pending 待定的狀態(tài)。
2. Promise 實(shí)例
Promise 實(shí)例生成以后,當(dāng) Promise 內(nèi)部狀態(tài)發(fā)生變化,可以使用 Promise.prototype.then() 方法獲取到。
const success = res => {
// 當(dāng)狀態(tài)從 pending 到 fulfilled 時(shí),執(zhí)行此函數(shù)
// some statements...
}
const fail = err => {
// 當(dāng)狀態(tài)從 pending 到 rejected 時(shí),執(zhí)行此函數(shù)
// some statements...
}
promise.then(success, fail)
then() 方法接受兩個(gè)回調(diào)函數(shù)作為參數(shù),第一個(gè)回調(diào)函數(shù)在 Promise 對(duì)象狀態(tài)變?yōu)?fulfilled 時(shí)被調(diào)用。第二回調(diào)函數(shù)在狀態(tài)變?yōu)?rejected 時(shí)被調(diào)用。then() 方法的兩個(gè)參數(shù)都是可選的。
注意,由于
Promise實(shí)例對(duì)象的Promise.prototype.then()、Promise.prototype.catch()、Promise.prototype.finally()方法屬于異步任務(wù)中的微任務(wù)。注意它們的執(zhí)行時(shí)機(jī),會(huì)在當(dāng)前同步任務(wù)執(zhí)行完之后,且在下一次宏任務(wù)執(zhí)行之前,被執(zhí)行。還有,
Promise構(gòu)造函數(shù)(即上述示例的handler函數(shù))內(nèi)部,仍屬于同步任務(wù),而非異步任務(wù)。所以,那個(gè)經(jīng)典的面試題就是,包括
setTimeout、Promise等,然后問(wèn)輸出順序是什么?本質(zhì)就是考察 JavaScript 的事件循環(huán)機(jī)制(Event Loop)嘛。這塊內(nèi)容可以看下文章:JavaScript 事件循環(huán)。
插了個(gè)話題,回來(lái)
then() 方法的兩個(gè)參數(shù) success()、fail(),它們接收的實(shí)參就是傳遞給 resolve() 和 reject() 的值。
例如:
function timeout(delay, status = true) {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
// 一般 reject 應(yīng)返回一個(gè) Error 實(shí)例對(duì)象,如:new Error('Oops')
status ? resolve('Success') : reject('Oops')
})
}, delay)
return promise
}
// 創(chuàng)建兩個(gè) Promise 實(shí)例對(duì)象
const p1 = timeout(1000)
const p2 = timeout(1000, false)
// pending -> fulfilled
p1.then(res => {
console.log(res) // "Success"
})
// pending -> rejected
p2.then(null, err => {
console.warn(err) // "Oops"
})
上面示例中,根據(jù) timeout 函數(shù)的邏輯,p1 實(shí)例的 Promise 狀態(tài)會(huì)從 pending -> fulfilled,而 p2 實(shí)例則是從 pending -> rejected。因此會(huì)分別打印出 "Success"、"Oops"。
例如,異步加載圖片的例子。
function loadImage(url) {
return new Promise((resolve, reject) => {
const image = new Image()
image.onload = function () {
resolve(image)
}
image.onerror = function () {
reject(new Error(`Could not load image at ${url}.`))
}
image.src = url
})
}
loadImage('https://jquery.com/jquery-wp-content/themes/jquery/images/logo-jquery@2x.png')
.then(
res => {
console.log('Image loaded successfully:', res)
},
err => {
console.warn(err)
}
)
因此,Promise 的用法還是很簡(jiǎn)單的,是預(yù)期結(jié)果的話,使用 resolve() 修改狀態(tài)為 fulfilled,非預(yù)期結(jié)果使用 reject() 修改狀態(tài)為 rejected。具體返回值根據(jù)實(shí)際場(chǎng)景返回就好。
3. Promise 注意事項(xiàng)
在構(gòu)建 Promise 對(duì)象的內(nèi)部,使用 resolve() 或 reject() 去改變 Promise 的狀態(tài),并不會(huì)終止 resolve 或 reject 后面代碼的執(zhí)行。
例如:
const promise = new Promise((resolve, reject) => {
resolve(1)
// 以下代碼仍會(huì)執(zhí)行,且會(huì)在 then 之前執(zhí)行。
// reject() 同理。
console.log(2)
})
promise.then(res => { console.log(res) }) // 先后打印出 2、1
若要終止后面的執(zhí)行,只要使用 return 關(guān)鍵字即可,類似 return resolve(1) 或 return reject(1)。但如果這樣,其實(shí)后面的代碼就沒(méi)意義,因此也就沒(méi)必要寫了。千萬(wàn)別在工作中寫出這樣的代碼,我怕你被打。這里只是為了說(shuō)明 resolve 或 reject 不會(huì)終止后面的代碼執(zhí)行而已。
一般來(lái)說(shuō),調(diào)用
resolve()或reject()說(shuō)明異步操作有了結(jié)果,那么Promise的使命就完成了,后續(xù)的操作應(yīng)該是放到then()方法里面,而不是放在resolve()或reject()后面。
在前面的示例中,resolve() 或 reject() 都是返回一個(gè)“普通值”。如果我們返回一個(gè) Promise 對(duì)象,會(huì)怎樣呢?
首先,它是允許返回一個(gè) Promise 對(duì)象的,但是有些區(qū)別。
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p1 success')
// reject('p1 fail')
}, 3000)
})
const p2 = new Promise((resolve, reject) => {
// 這時(shí)返回一個(gè) Promise 對(duì)象
// ?? 注意,這里 resolve(p1) 或 reject(p1) 執(zhí)行的邏輯會(huì)有所不同。
resolve(p1)
// reject(p1)
})
p2.then(
res => {
console.log('p2 then:', res)
},
err => {
console.log('p2 catch:', err)
}
)
分析如下:
1. 若 p2 內(nèi)里面 `resolve(p1)` 時(shí):
當(dāng)代碼執(zhí)行到 `resolve(p1)` 時(shí),由于 p1 的狀態(tài)仍是 pending,
這時(shí) p1 的狀態(tài)會(huì)傳遞給 p2,也就是說(shuō) p1 的狀態(tài)決定了 p2 的狀態(tài),
因此 `p2.then()` 需要等 p1 的狀態(tài)發(fā)生變化,才會(huì)被調(diào)用,
且 `p2.then()` 獲取到的狀態(tài)就是 p1 的狀態(tài)
假設(shè)代碼執(zhí)行到 `resolve(p1)` 時(shí),若 p1 的狀態(tài)已定型,即 fulfilled 或 rejected,
會(huì)立即調(diào)用 `p2.then()` 方法。
PS:這里“立即”是指,當(dāng)前同步任務(wù)已執(zhí)行完畢的前提下。第 2 點(diǎn)也是如此。
2. 若 p2 內(nèi)是 `reject(p1)` 時(shí),情況會(huì)有所不同:
當(dāng)代碼執(zhí)行到 `reject(p1)` 時(shí),由于 p2 的狀態(tài)會(huì)變更為 rejected,
接著會(huì)立即調(diào)用 `p2.then()` 方法,由于是 rejected 狀態(tài),
因此,會(huì)觸發(fā) `p2.then()` 的第二個(gè)參數(shù),此時(shí) err 的值就是 p1(一個(gè) Promise 對(duì)象)。
假設(shè) p1 的狀態(tài)最終變成了 rejected,那么 err 還要捕獲異常,
例如 `err.catch(err => { /* do something... */ })`,
否則的話,在控制臺(tái)會(huì)報(bào)錯(cuò),類似:"Uncaught (in promise) p1 fail",
原因就是 Promise 對(duì)象的 rejected 狀態(tài)未處理,導(dǎo)致的。
假設(shè) p1 的狀態(tài)最終變成 fulfilled,那么不需要做上一步類似的處理。
上面兩種情況,其實(shí)相當(dāng)于 Promise.resolve(p1)、Promise.reject(p1)。我們來(lái)打印一下兩種結(jié)果:
當(dāng) p1 狀態(tài)為 fulfilled 時(shí),p2 狀態(tài)如圖:

當(dāng) p1 狀態(tài)為 rejected 時(shí),p2 狀態(tài)如圖:

三、Promise.prototype.then()
Promise 的實(shí)例具有 then() 方法,它是定義在原型對(duì)象 Promise.prototype 上的。當(dāng) Promise 實(shí)例對(duì)象的狀態(tài)發(fā)生變化,此方法就會(huì)被觸發(fā)調(diào)用。
前面提到 Promise.prototype.then() 接受兩個(gè)參數(shù),兩者均可選,這里不再贅述。
then() 方法返回一個(gè)新的 Promise 實(shí)例對(duì)象(注意,不是原來(lái)那個(gè) Promise 實(shí)例),也因此可以采用鏈?zhǔn)綄懛?,?then() 方法后面可以再調(diào)用另一個(gè) then() 方法。
例如,以下示例使用 Fetch API 進(jìn)行網(wǎng)絡(luò)請(qǐng)求:
window.fetch('/config')
.then(response => response.json())
.then(
res => {
// do something...
},
err => {
// do something...
}
)
// .then() // ...
以上鏈?zhǔn)秸{(diào)用,會(huì)按照順序調(diào)用回調(diào)函數(shù),后一個(gè) then() 的執(zhí)行,需等到前一個(gè) Promise 對(duì)象的狀態(tài)定型。
四、Promise.prototype.catch()
Promise.prototype.catch() 方法是 then(null, rejection) 或 then(undefined, rejection) 的別名,用于指定發(fā)生錯(cuò)誤時(shí)的回調(diào)函數(shù)。
同樣地,它會(huì)返回一個(gè)新的 Promise 實(shí)例對(duì)象。
const promise = new Promise((resolve, reject) => {
reject('Oops') // 或通過(guò) throw 方式主動(dòng)拋出錯(cuò)誤,使其變成 rejected 狀態(tài)
// 但注意的是,前面狀態(tài)“定型”之后,狀態(tài)是不會(huì)再變的。
// 這后面試圖改變狀態(tài),或主動(dòng)拋出錯(cuò)誤,或出現(xiàn)其他語(yǔ)法錯(cuò)誤,
// 不會(huì)被外部捕獲到,即無(wú)意義。
})
promise.catch(err => {
console.log(err) // "Oops"
})
// 相當(dāng)于
promise.then(
null,
err => {
console.log(err) // "Oops"
}
)
通過(guò) throw 等方式使其變成 rejected 狀態(tài),相當(dāng)于:
const promise = new Promise((resolve, reject) => {
try {
throw 'Oops'
// 一般地,是拋出一個(gè) Error(或派生)實(shí)例對(duì)象,如 throw new Error('Oops')
} catch (e) {
reject(e)
}
})
五、捕獲 rejected 狀態(tài)的兩種方式比較
前面提到有兩種方式,可以捕獲 Promise 對(duì)象的 rejected 狀態(tài)。那么孰優(yōu)孰劣呢?
建議如下:
盡量不要在
Promise.prototype.then()方法里面定義onRejection回調(diào)函數(shù)(即then()的第二個(gè)參數(shù)),總使用Promise.prototype.catch()方法。
const promise = new Promise((resolve, reject) => {
// some statements
})
// bad
promise.then(
res => { /* some statements */ },
err => { /* some statements */ }
)
// good
promise
.then(res => { /* some statements */ })
.catch(err => { /* some statements */ })
上面示例中,第二種寫法要好于第一種寫法。理由是第二種寫法可以捕獲前面 then() 方法中的異?;蝈e(cuò)誤,也更接近同步寫法(try...catch)。因此,建議總是使用 Promise.prototype.catch() 方法。
與傳統(tǒng)的 try...catch 代碼塊不同的是,即使 Promise 內(nèi)部出現(xiàn)錯(cuò)誤,也不會(huì)影響 Promise 外部代碼的執(zhí)行。
const promise = new Promise((resolve, reject) => {
say() // 這行會(huì)報(bào)錯(cuò):ReferenceError: say is not defined
})
promise.then(res => { /* some statements */ })
setTimeout(() => {
console.log(promise) // 這里仍會(huì)執(zhí)行,打印出 promise 實(shí)例對(duì)象
})

上面的示例中,在 Promise 內(nèi)部就會(huì)發(fā)生引用錯(cuò)誤,因?yàn)?say 函數(shù)并沒(méi)有定義,但并未終止腳本的執(zhí)行。接著還會(huì)輸出 promise 對(duì)象。也就是說(shuō),Promise 內(nèi)部的錯(cuò)誤并不會(huì)影響到 Promise 外部代碼,通俗的說(shuō)法就是“Promise 會(huì)吃掉錯(cuò)誤”。
但是,如果腳本放在服務(wù)器上執(zhí)行,退出碼就是 0(表示執(zhí)行成功)。不過(guò) Node.js 有一個(gè) unhandledRejection 事件,它專門監(jiān)聽未捕獲的 reject 錯(cuò)誤,腳本會(huì)觸發(fā)這個(gè)事件的監(jiān)聽函數(shù),可以在監(jiān)聽函數(shù)里面拋出錯(cuò)誤。如下:

注意,Node.js 有計(jì)劃在未來(lái)廢除 unhandledRejection 事件。如果 Promise 內(nèi)部由未捕獲的錯(cuò)誤,會(huì)直接終止進(jìn)程,并且進(jìn)程的退出碼不為 0。
在 catch() 方法中,也可以拋出錯(cuò)誤。而且由于 then() 和 catch() 方法均返回一個(gè)新的 Promise 實(shí)例對(duì)象,因此可以采用鏈?zhǔn)綄懛?,寫出一系列?..
const promise = new Promise((resolve, reject) => {
reject('Oops')
})
promise
.then(res => { /* some statements */ })
.catch(err => { throw new Error('Oh...') })
.catch(err => { /* 這里可以捕獲上一個(gè) rejected 狀態(tài) */ })
// ... 還可以寫一系列的 then、catch 方法
六、Promise.prototype.finally()
在 ES9 標(biāo)準(zhǔn)中,引入了 Promise.prototype.finally() 方法,用于指定 Promise 對(duì)象狀態(tài)發(fā)生改變(不管 fulfilled 還是 rejected)后,都會(huì)觸發(fā)此方法。
const promise = new Promise((resolve, reject) => {
// some statements
})
promise
.then(res => { /* some statements */ })
.catch(err => { /* some statements */ })
.finally(() => {
// do something...
// 注意,finally 不接受任何參數(shù),自然也無(wú)法得知 Promise 對(duì)象的狀態(tài)。
})
若 Promise 內(nèi)部不寫任何
resovle()、或rejected()、或無(wú)任何語(yǔ)法錯(cuò)誤(如上述示例),Promise實(shí)例對(duì)象的狀態(tài)并不會(huì)發(fā)生變化,即一直都是pending狀態(tài),它都不會(huì)觸發(fā)then()、catch()、finally()方法。這點(diǎn)就怕有人會(huì)誤解,狀態(tài)不發(fā)生變化時(shí)也會(huì)觸發(fā)finally()方法,這是錯(cuò)的。
Promise.prototype.finally() 也是返回一個(gè)新的 Promise 實(shí)例對(duì)象,而且該實(shí)例對(duì)象的值,就是前面一個(gè) Promise 實(shí)例對(duì)象的值。
const p1 = new Promise(resolve => resolve(1))
const p2 = p1.then().finally()
const p3 = p1.then(() => { }).finally()
const p4 = p1.then(() => { return true }).finally()
const p5 = p1.then(() => { throw 'Oops' /* 當(dāng)然這里沒(méi)處理 rejected 狀態(tài) */ }).finally()
const p6 = p1.then(() => { throw 'Oh...' }).catch(err => { return 'abc' }).finally()
const p7 = p1.finally(() => { return 'finally' })
const p8 = p1.finally(() => { throw 'error' })
setTimeout(() => {
console.log('p1:', p1)
console.log('p2:', p2)
console.log('p3:', p3)
console.log('p4:', p4)
console.log('p5:', p5)
console.log('p6:', p6)
console.log('p7:', p7)
console.log('p8:', p8)
})
// 解釋一下 `p1` 和 `p1.then()`:
// 當(dāng) `then()` 方法中不寫回調(diào)函數(shù)時(shí),會(huì)發(fā)生值的穿透,
// 即 `p1.then()` 返回的新實(shí)例對(duì)象(假設(shè)為 `x`)的值跟 p1 實(shí)例的值是一樣的,
// 但注意 `p1` 和 `x` 是兩個(gè)不同的 Promise 實(shí)例對(duì)象。
// 關(guān)于值穿透的問(wèn)題,后面會(huì)給出示例。

根據(jù)打印結(jié)果可以驗(yàn)證: finally() 方法返回的 Promise 實(shí)例對(duì)象的值與前一個(gè) Promise 實(shí)例對(duì)象的值是相等的,但盡管如此,兩者是兩個(gè)不同的 Promise 實(shí)例對(duì)象??梢源蛴∫幌?p1 === p7,比較結(jié)果為 false。
關(guān)于 Promise.prototype.finally() 的實(shí)現(xiàn),如下:
Promise.prototype.finally = function (callback) {
let P = this.constructor
return this.then(
value => P.resolve(callback && callback()).then(() => value),
reason => P.resolve(callback && callback()).then(() => { throw reason })
)
}
七、總結(jié)
關(guān)于 Promise.prototype.then()、Promise.prototype.catch()、Promise.prototype.finally() 方法,總結(jié)以下特點(diǎn):
三者均返回一個(gè)全新的
Promise實(shí)例對(duì)象。即使
then()、catch()、finally()方法在不指定回調(diào)函數(shù)的情況下,仍會(huì)返回一個(gè)全新的Promise實(shí)例對(duì)象,但此時(shí)會(huì)出現(xiàn)“值穿透”的情況,即實(shí)例值為前一個(gè)實(shí)例的值。假設(shè)三者的回調(diào)函數(shù)中無(wú)語(yǔ)法錯(cuò)誤(包括不使用
throw關(guān)鍵字) 時(shí),then()和catch()方法返回的實(shí)例對(duì)象的值,依靠return關(guān)鍵字來(lái)指定,否則為undefined。而
finally()方法稍有不同,即使使用了return也是無(wú)意義的,因?yàn)樗祷氐?Promise實(shí)例對(duì)象的值總是前一個(gè)Promise實(shí)例的值。三個(gè)方法的返回操作
return any,相當(dāng)于Promise.resolve(any)(這里any是指任何值)。當(dāng)
then()、catch()、finally()方法中出現(xiàn)語(yǔ)法錯(cuò)誤或者利用throw關(guān)鍵字主動(dòng)拋出錯(cuò)誤,它們返回的Promise實(shí)例對(duì)象的狀態(tài)會(huì)變成rejected,而且實(shí)例對(duì)象的值就是所拋出的錯(cuò)誤原因。
Promise對(duì)象的錯(cuò)誤具有“冒泡”性質(zhì),會(huì)一直向后傳遞,直到被捕獲為止。也就是說(shuō),錯(cuò)誤總是會(huì)被下一個(gè)catch()方法捕獲。
關(guān)于值的“穿透”,請(qǐng)看示例:
const person = { name: 'Frankie' } // 使用引用值更能說(shuō)明問(wèn)題
const p1 = new Promise(resolve => resolve(person))
const p2 = new Promise((resolve, reject) => reject(person))
// 情況一:fulfilled
p1.then(res => {
console.log(res === person) // true
})
// 情況二:fulfilled
p1
.then()
.then(res => {
console.log(res === person) // true
})
// 情況三:rejected
p2
.catch()
.then(res => { /* 不會(huì)觸發(fā) then */ })
.catch(err => {
console.log(err === person) // true
})
// 情況四:fulfilled
p1
.finally()
.then(res => {
console.log(res === person) // true
})
從結(jié)果上看,盡管三者在不指定回調(diào)函數(shù)的情形下,“似乎”是不影響結(jié)果的。但前面提到 p1 跟 p1.then()、p1.catch()、p1.finally() 都是兩個(gè)不同的 Promise 實(shí)例對(duì)象,盡管這些實(shí)例對(duì)象的值是相等的。
在實(shí)際應(yīng)用場(chǎng)景中,我們應(yīng)該避免寫出這些“無(wú)意義”的代碼。但是我們?cè)谌W(xué)習(xí)它們的時(shí)候,應(yīng)該要知道。就是“用不用”和“會(huì)不會(huì)”是兩回事。
下一篇接著介紹 Promise.all()、Promise.race() 等,未完待續(xù)...