我認為 Promise 應該算是 ES6 標準最大的亮點,它提供了異步編程的一種解決方案街立。比傳統(tǒng)的回調函數(shù)和事件解決方案滨溉,它更合理、更強大长赞。
一晦攒、簡介
Promise 是一個容器,里面保存著某個未來才會結束的事件(一般為異步操作)的結果得哆。從語法上來說脯颜,Promise
是一個對象,它可以獲取異步操作的消息贩据。
Promise
對象的特點:
Promise
對象有且只有三種狀態(tài):pending
栋操、fulfilled
、rejected
饱亮,分別表示進行中矾芙、已成功、已失敗近上。一旦狀態(tài)發(fā)生改變剔宪,就不會再變。狀態(tài)的改變只有兩種可能:
pending -> fulfilled
或pending -> rejected
。若發(fā)生了其中一種情況葱绒,狀態(tài)就會一直保存這個結果感帅,這時就成為resolved
(已定型)。
這種以“同步的方式”去表達異步流程地淀,可以避免層層嵌套的回調函數(shù)失球,避免出現(xiàn)“回調地獄”(Callback Hell)。
BTW帮毁,網上有些文章把
fulfilled
狀態(tài)实苞,叫成resolved
,盡管我們可能知道他想表達的意思作箍,但其實是不對的硬梁。
Promise
對象的缺點:
一是無法取消 Promise
,一旦創(chuàng)建它就會立即執(zhí)行胞得,無法中途取消荧止;二是若不設置回調函數(shù)情況下,Promise
內部拋出錯誤阶剑,不會反饋到外部跃巡;三是當處于 pending
狀態(tài),無法得知目前進展到哪個階段牧愁。
二素邪、Promise 用法
根據(jù)規(guī)定,Promise
是一個構造函數(shù)猪半,用來生成 Promise
實例對象兔朦。
1. 創(chuàng)建 Promise 對象
示例:
const handler = (resolve, reject) => {
// some statements...
// 根據(jù)異步操作的結果,通過 resolve 或 reject 函數(shù)去改變 Promise 對象的狀態(tài)
if (true) {
// pending -> fulfilled
resolve(...)
} else {
// pending -> rejected
reject(...)
}
// 需要注意的是:
// 1. 在上面 Promise 狀態(tài)已經定型(fulfilled 或 rejected)磨确,
// 因此沽甥,我們再使用 resolve() 或 reject() 或主動/被動拋出錯誤的方式,
// 試圖再次修改狀態(tài)乏奥,是沒用的摆舟,狀態(tài)不會再發(fā)生改變。
// 2. 當 Promise 對象的狀態(tài)“已定型”后邓了,若未使用 return 終止代碼往下執(zhí)行恨诱,
// 后面代碼出現(xiàn)的錯誤(主動拋出或語法錯誤等),在外部都不可見骗炉,無法捕獲到照宝。
// 3. hander 函數(shù)的返回值是沒意義的。怎么理解痕鳍?
// 假設內部不包括 resolve() 或 reject() 或內部不出現(xiàn)語法錯誤硫豆,
// 或不主動拋出錯誤龙巨,僅有類似 `return 'anything'` 語句,
// 那么 promise 對象永遠都是 pending 狀態(tài)熊响。
}
const promise = new Promise(handler)
Promie
構造函數(shù)接受一個函數(shù)作為參數(shù)旨别,該函數(shù)的兩個參數(shù)分別是 resolve
和 reject
。而 resolve
和 rejeact
也是函數(shù)汗茄,其作用是改變 Promise
對象的狀態(tài)秸弛,分別是 pending -> fulfilled
和 pending -> rejected
。
假設構造函數(shù)內不指定 resolve
或 reject
函數(shù)洪碳,那么 Promise
的對象會一直保持著 pending
待定的狀態(tài)递览。
2. Promise 實例
Promise
實例生成以后,當 Promise
內部狀態(tài)發(fā)生變化瞳腌,可以使用 Promise.prototype.then()
方法獲取到绞铃。
const success = res => {
// 當狀態(tài)從 pending 到 fulfilled 時,執(zhí)行此函數(shù)
// some statements...
}
const fail = err => {
// 當狀態(tài)從 pending 到 rejected 時嫂侍,執(zhí)行此函數(shù)
// some statements...
}
promise.then(success, fail)
then()
方法接受兩個回調函數(shù)作為參數(shù)儿捧,第一個回調函數(shù)在 Promise
對象狀態(tài)變?yōu)?fulfilled
時被調用。第二回調函數(shù)在狀態(tài)變?yōu)?rejected
時被調用挑宠。then()
方法的兩個參數(shù)都是可選的菲盾。
注意,由于
Promise
實例對象的Promise.prototype.then()
各淀、Promise.prototype.catch()
懒鉴、Promise.prototype.finally()
方法屬于異步任務中的微任務。注意它們的執(zhí)行時機碎浇,會在當前同步任務執(zhí)行完之后临谱,且在下一次宏任務執(zhí)行之前,被執(zhí)行奴璃。還有吴裤,
Promise
構造函數(shù)(即上述示例的handler
函數(shù))內部,仍屬于同步任務溺健,而非異步任務。所以钮蛛,那個經典的面試題就是鞭缭,包括
setTimeout
、Promise
等魏颓,然后問輸出順序是什么岭辣?本質就是考察 JavaScript 的事件循環(huán)機制(Event Loop)嘛。這塊內容可以看下文章:JavaScript 事件循環(huán)甸饱。
插了個話題沦童,回來
then()
方法的兩個參數(shù) success()
仑濒、fail()
,它們接收的實參就是傳遞給 resolve()
和 reject()
的值偷遗。
例如:
function timeout(delay, status = true) {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
// 一般 reject 應返回一個 Error 實例對象墩瞳,如:new Error('Oops')
status ? resolve('Success') : reject('Oops')
})
}, delay)
return promise
}
// 創(chuàng)建兩個 Promise 實例對象
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
實例的 Promise
狀態(tài)會從 pending -> fulfilled
喉酌,而 p2
實例則是從 pending -> rejected
。因此會分別打印出 "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
的用法還是很簡單的,是預期結果的話鲜锚,使用 resolve()
修改狀態(tài)為 fulfilled
突诬,非預期結果使用 reject()
修改狀態(tài)為 rejected
。具體返回值根據(jù)實際場景返回就好烹棉。
3. Promise 注意事項
在構建 Promise
對象的內部攒霹,使用 resolve()
或 reject()
去改變 Promise
的狀態(tài),并不會終止 resolve
或 reject
后面代碼的執(zhí)行浆洗。
例如:
const promise = new Promise((resolve, reject) => {
resolve(1)
// 以下代碼仍會執(zhí)行催束,且會在 then 之前執(zhí)行。
// reject() 同理伏社。
console.log(2)
})
promise.then(res => { console.log(res) }) // 先后打印出 2抠刺、1
若要終止后面的執(zhí)行,只要使用 return
關鍵字即可摘昌,類似 return resolve(1)
或 return reject(1)
速妖。但如果這樣,其實后面的代碼就沒意義聪黎,因此也就沒必要寫了罕容。千萬別在工作中寫出這樣的代碼,我怕你被打稿饰。這里只是為了說明 resolve
或 reject
不會終止后面的代碼執(zhí)行而已锦秒。
一般來說,調用
resolve()
或reject()
說明異步操作有了結果喉镰,那么Promise
的使命就完成了旅择,后續(xù)的操作應該是放到then()
方法里面,而不是放在resolve()
或reject()
后面侣姆。
在前面的示例中生真,resolve()
或 reject()
都是返回一個“普通值”沉噩。如果我們返回一個 Promise
對象,會怎樣呢柱蟀?
首先川蒙,它是允許返回一個 Promise
對象的,但是有些區(qū)別产弹。
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p1 success')
// reject('p1 fail')
}, 3000)
})
const p2 = new Promise((resolve, reject) => {
// 這時返回一個 Promise 對象
// ?? 注意派歌,這里 resolve(p1) 或 reject(p1) 執(zhí)行的邏輯會有所不同。
resolve(p1)
// reject(p1)
})
p2.then(
res => {
console.log('p2 then:', res)
},
err => {
console.log('p2 catch:', err)
}
)
分析如下:
1. 若 p2 內里面 `resolve(p1)` 時:
當代碼執(zhí)行到 `resolve(p1)` 時痰哨,由于 p1 的狀態(tài)仍是 pending胶果,
這時 p1 的狀態(tài)會傳遞給 p2扶叉,也就是說 p1 的狀態(tài)決定了 p2 的狀態(tài)厂置,
因此 `p2.then()` 需要等 p1 的狀態(tài)發(fā)生變化,才會被調用碑韵,
且 `p2.then()` 獲取到的狀態(tài)就是 p1 的狀態(tài)
假設代碼執(zhí)行到 `resolve(p1)` 時撬讽,若 p1 的狀態(tài)已定型蕊连,即 fulfilled 或 rejected,
會立即調用 `p2.then()` 方法游昼。
PS:這里“立即”是指甘苍,當前同步任務已執(zhí)行完畢的前提下。第 2 點也是如此烘豌。
2. 若 p2 內是 `reject(p1)` 時载庭,情況會有所不同:
當代碼執(zhí)行到 `reject(p1)` 時,由于 p2 的狀態(tài)會變更為 rejected廊佩,
接著會立即調用 `p2.then()` 方法囚聚,由于是 rejected 狀態(tài),
因此标锄,會觸發(fā) `p2.then()` 的第二個參數(shù)顽铸,此時 err 的值就是 p1(一個 Promise 對象)。
假設 p1 的狀態(tài)最終變成了 rejected料皇,那么 err 還要捕獲異常谓松,
例如 `err.catch(err => { /* do something... */ })`,
否則的話践剂,在控制臺會報錯毒返,類似:"Uncaught (in promise) p1 fail",
原因就是 Promise 對象的 rejected 狀態(tài)未處理舷手,導致的。
假設 p1 的狀態(tài)最終變成 fulfilled劲绪,那么不需要做上一步類似的處理男窟。
上面兩種情況盆赤,其實相當于 Promise.resolve(p1)
、Promise.reject(p1)
歉眷。我們來打印一下兩種結果:
當 p1
狀態(tài)為 fulfilled
時牺六,p2
狀態(tài)如圖:
當 p1
狀態(tài)為 rejected
時,p2
狀態(tài)如圖:
三汗捡、Promise.prototype.then()
Promise
的實例具有 then()
方法淑际,它是定義在原型對象 Promise.prototype
上的。當 Promise
實例對象的狀態(tài)發(fā)生變化扇住,此方法就會被觸發(fā)調用春缕。
前面提到 Promise.prototype.then()
接受兩個參數(shù),兩者均可選艘蹋,這里不再贅述锄贼。
then()
方法返回一個新的 Promise
實例對象(注意,不是原來那個 Promise
實例)女阀,也因此可以采用鏈式寫法宅荤,即 then()
方法后面可以再調用另一個 then()
方法。
例如浸策,以下示例使用 Fetch API 進行網絡請求:
window.fetch('/config')
.then(response => response.json())
.then(
res => {
// do something...
},
err => {
// do something...
}
)
// .then() // ...
以上鏈式調用冯键,會按照順序調用回調函數(shù),后一個 then()
的執(zhí)行庸汗,需等到前一個 Promise
對象的狀態(tài)定型惫确。
四、Promise.prototype.catch()
Promise.prototype.catch()
方法是 then(null, rejection)
或 then(undefined, rejection)
的別名夫晌,用于指定發(fā)生錯誤時的回調函數(shù)雕薪。
同樣地,它會返回一個新的 Promise
實例對象晓淀。
const promise = new Promise((resolve, reject) => {
reject('Oops') // 或通過 throw 方式主動拋出錯誤所袁,使其變成 rejected 狀態(tài)
// 但注意的是,前面狀態(tài)“定型”之后凶掰,狀態(tài)是不會再變的燥爷。
// 這后面試圖改變狀態(tài),或主動拋出錯誤懦窘,或出現(xiàn)其他語法錯誤前翎,
// 不會被外部捕獲到,即無意義畅涂。
})
promise.catch(err => {
console.log(err) // "Oops"
})
// 相當于
promise.then(
null,
err => {
console.log(err) // "Oops"
}
)
通過 throw
等方式使其變成 rejected
狀態(tài)港华,相當于:
const promise = new Promise((resolve, reject) => {
try {
throw 'Oops'
// 一般地,是拋出一個 Error(或派生)實例對象午衰,如 throw new Error('Oops')
} catch (e) {
reject(e)
}
})
五立宜、捕獲 rejected 狀態(tài)的兩種方式比較
前面提到有兩種方式冒萄,可以捕獲 Promise
對象的 rejected
狀態(tài)。那么孰優(yōu)孰劣呢橙数?
建議如下:
盡量不要在
Promise.prototype.then()
方法里面定義onRejection
回調函數(shù)(即then()
的第二個參數(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()
方法中的異常或錯誤钟哥,也更接近同步寫法(try...catch
)迎献。因此,建議總是使用 Promise.prototype.catch()
方法瞪醋。
與傳統(tǒng)的 try...catch
代碼塊不同的是忿晕,即使 Promise 內部出現(xiàn)錯誤,也不會影響 Promise 外部代碼的執(zhí)行银受。
const promise = new Promise((resolve, reject) => {
say() // 這行會報錯:ReferenceError: say is not defined
})
promise.then(res => { /* some statements */ })
setTimeout(() => {
console.log(promise) // 這里仍會執(zhí)行践盼,打印出 promise 實例對象
})
上面的示例中,在 Promise 內部就會發(fā)生引用錯誤宾巍,因為 say
函數(shù)并沒有定義咕幻,但并未終止腳本的執(zhí)行。接著還會輸出 promise
對象顶霞。也就是說肄程,Promise 內部的錯誤并不會影響到 Promise 外部代碼,通俗的說法就是“Promise 會吃掉錯誤”选浑。
但是蓝厌,如果腳本放在服務器上執(zhí)行,退出碼就是 0
(表示執(zhí)行成功)古徒。不過 Node.js 有一個 unhandledRejection
事件拓提,它專門監(jiān)聽未捕獲的 reject
錯誤,腳本會觸發(fā)這個事件的監(jiān)聽函數(shù)隧膘,可以在監(jiān)聽函數(shù)里面拋出錯誤代态。如下:
注意,Node.js 有計劃在未來廢除 unhandledRejection
事件疹吃。如果 Promise 內部由未捕獲的錯誤蹦疑,會直接終止進程,并且進程的退出碼不為 0
萨驶。
在 catch()
方法中歉摧,也可以拋出錯誤。而且由于 then()
和 catch()
方法均返回一個新的 Promise
實例對象,因此可以采用鏈式寫法叁温,寫出一系列的...
const promise = new Promise((resolve, reject) => {
reject('Oops')
})
promise
.then(res => { /* some statements */ })
.catch(err => { throw new Error('Oh...') })
.catch(err => { /* 這里可以捕獲上一個 rejected 狀態(tài) */ })
// ... 還可以寫一系列的 then豆挽、catch 方法
六、Promise.prototype.finally()
在 ES9 標準中券盅,引入了 Promise.prototype.finally()
方法,用于指定 Promise
對象狀態(tài)發(fā)生改變(不管 fulfilled
還是 rejected
)后膛檀,都會觸發(fā)此方法锰镀。
const promise = new Promise((resolve, reject) => {
// some statements
})
promise
.then(res => { /* some statements */ })
.catch(err => { /* some statements */ })
.finally(() => {
// do something...
// 注意,finally 不接受任何參數(shù)咖刃,自然也無法得知 Promise 對象的狀態(tài)泳炉。
})
若 Promise 內部不寫任何
resovle()
、或rejected()
嚎杨、或無任何語法錯誤(如上述示例)花鹅,Promise
實例對象的狀態(tài)并不會發(fā)生變化,即一直都是pending
狀態(tài)枫浙,它都不會觸發(fā)then()
刨肃、catch()
、finally()
方法箩帚。這點就怕有人會誤解真友,狀態(tài)不發(fā)生變化時也會觸發(fā)finally()
方法,這是錯的紧帕。
Promise.prototype.finally()
也是返回一個新的 Promise
實例對象盔然,而且該實例對象的值,就是前面一個 Promise
實例對象的值是嗜。
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' /* 當然這里沒處理 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()`:
// 當 `then()` 方法中不寫回調函數(shù)時愈案,會發(fā)生值的穿透,
// 即 `p1.then()` 返回的新實例對象(假設為 `x`)的值跟 p1 實例的值是一樣的鹅搪,
// 但注意 `p1` 和 `x` 是兩個不同的 Promise 實例對象站绪。
// 關于值穿透的問題,后面會給出示例涩嚣。
根據(jù)打印結果可以驗證: finally()
方法返回的 Promise
實例對象的值與前一個 Promise
實例對象的值是相等的崇众,但盡管如此,兩者是兩個不同的 Promise
實例對象航厚∏旮瑁可以打印一下 p1 === p7
,比較結果為 false
幔睬。
關于 Promise.prototype.finally()
的實現(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 })
)
}
七、總結
關于 Promise.prototype.then()
、Promise.prototype.catch()
赦抖、Promise.prototype.finally()
方法舱卡,總結以下特點:
三者均返回一個全新的
Promise
實例對象。即使
then()
队萤、catch()
轮锥、finally()
方法在不指定回調函數(shù)的情況下,仍會返回一個全新的Promise
實例對象要尔,但此時會出現(xiàn)“值穿透”的情況舍杜,即實例值為前一個實例的值。假設三者的回調函數(shù)中無語法錯誤(包括不使用
throw
關鍵字) 時赵辕,then()
和catch()
方法返回的實例對象的值既绩,依靠return
關鍵字來指定,否則為undefined
还惠。而
finally()
方法稍有不同饲握,即使使用了return
也是無意義的,因為它返回的Promise
實例對象的值總是前一個Promise
實例的值蚕键。三個方法的返回操作
return any
救欧,相當于Promise.resolve(any)
(這里any
是指任何值)。當
then()
嚎幸、catch()
颜矿、finally()
方法中出現(xiàn)語法錯誤或者利用throw
關鍵字主動拋出錯誤,它們返回的Promise
實例對象的狀態(tài)會變成rejected
嫉晶,而且實例對象的值就是所拋出的錯誤原因骑疆。
Promise
對象的錯誤具有“冒泡”性質,會一直向后傳遞替废,直到被捕獲為止箍铭。也就是說,錯誤總是會被下一個catch()
方法捕獲椎镣。
關于值的“穿透”诈火,請看示例:
const person = { name: 'Frankie' } // 使用引用值更能說明問題
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 => { /* 不會觸發(fā) then */ })
.catch(err => {
console.log(err === person) // true
})
// 情況四:fulfilled
p1
.finally()
.then(res => {
console.log(res === person) // true
})
從結果上看,盡管三者在不指定回調函數(shù)的情形下状答,“似乎”是不影響結果的冷守。但前面提到 p1
跟 p1.then()
、p1.catch()
惊科、p1.finally()
都是兩個不同的 Promise
實例對象拍摇,盡管這些實例對象的值是相等的。
在實際應用場景中馆截,我們應該避免寫出這些“無意義”的代碼充活。但是我們在去學習它們的時候蜂莉,應該要知道。就是“用不用”和“會不會”是兩回事混卵。
下一篇接著介紹 Promise.all()
映穗、Promise.race()
等,未完待續(xù)...