通過前面幾節(jié)課的學(xué)習(xí),我們認(rèn)識到:想優(yōu)雅地進(jìn)行異步操作,必須要熟識一個極其重要的概念 —— Promise。它是取代傳統(tǒng)回調(diào),實現(xiàn)同步鏈?zhǔn)綄懛ǖ慕鉀Q方案;是理解 generator、async/await 的關(guān)鍵。但是 Promise 對于初學(xué)者來說,并不是很好理解,其中的概念紛雜,且抽象程度較高。
與此同時,在中高級前端開發(fā)面試當(dāng)中,對于 Promise 的考察也多種多樣,近幾年流行「讓開發(fā)者實現(xiàn)一個 Promise」。那么這一講,我就帶大家實現(xiàn)一個簡單的 Promise。注意:實現(xiàn)不是最終目的,在實現(xiàn)的過程中,我會配以關(guān)鍵結(jié)論和關(guān)于 Promise 的考察題目,希望大家可以融會貫通。
整個過程將分兩節(jié)課完成,本講的相關(guān)知識點如下:
從 Promise 化一個 API 談起
熟悉微信小程序開發(fā)的讀者應(yīng)該知道,我們使用 wx.request() 在微信小程序環(huán)境中發(fā)送一個網(wǎng)絡(luò)請求。參考官方文檔,具體用法如下:
wx.request({
url: 'test.php', // 僅為示例,并非真實的接口地址
data: {
x: '',
y: ''
},
header: {
'content-type': 'application/json' // 默認(rèn)值
},
success(res) {
console.log(res.data)
}
})
配置化的 API 風(fēng)格和我們早期使用 jQuery 中 Ajax 方法的封裝類似。這樣的設(shè)計有一個小的問題,就是容易出現(xiàn)「回調(diào)地獄」問題。如果我們想先通過 ./userInfo 接口來獲取登錄用戶信息數(shù)據(jù),再從登錄用戶信息數(shù)據(jù)中,通過請求 ./${id}/friendList 接口來獲取登錄用戶所有好友列表,就需要:
wx.request({
url: './userInfo',
success(res) {
const id = res.data.id
wx.request({
url: `./${id}/friendList`,
success(res) {
console.log(res)
}
})
}
})
這只是嵌套了一層回調(diào)而已,還夠不成「地獄」場景,但是足以說明問題。
我們知道解決「回調(diào)地獄」問題的一個極佳方式就是 Promise,將微信小程序 wx.request() 方法進(jìn)行 Promise 化:
const wxRequest = (url, data = {}, method = 'GET') =>
new Promise((resolve, reject) => {
wx.request({
url,
data,
method,
header: {
//通用化 header 設(shè)置
},
success: function (res) {
const code = res.statusCode
if (code !== 200) {
reject({ error: 'request fail', code })
return
}
resolve(res.data)
},
fail: function (res) {
reject({ error: 'request fail'})
},
})
})
Promise 基本概念不再過多介紹。這是一個典型的 Promise 化案例,當(dāng)然我們不僅可以對 wx.request() API 進(jìn)行 Promise 化,更應(yīng)該做的通用,能夠 Promise 化更多類似(通過 success 和 fail 表征狀態(tài))的接口:
const promisify = fn => args =>
new Promise((resolve, reject) => {
args.success = function(res) {
return resolve(res)
}
args.fail = function(res) {
return reject(res)
}
})
使用:
const wxRequest = promisify(wx.request)
通過上例,我們知道:
Promise 其實就是一個構(gòu)造函數(shù),我們使用這個構(gòu)造函數(shù)創(chuàng)建一個 Promise 實例。該構(gòu)造函數(shù)很簡單,它只有一個參數(shù),按照 Promise/A+ 規(guī)范的命名,把 Promise 構(gòu)造函數(shù)的參數(shù)叫做 executor,executor 類型為函數(shù)。這個函數(shù)又「自動」具有 resolve、reject 兩個方法作為參數(shù)。
請仔細(xì)體會上述結(jié)論,那么我們可以通過結(jié)論,開始實現(xiàn) Promise 的第一步:
function Promise(executor) {
}
好吧,初始起步是夠基本的了。如果讀者還不理解構(gòu)造函數(shù)的概念,我給大家推薦閱讀: 構(gòu)造函數(shù)與 new 命令,在理解的基礎(chǔ)上,讓我們繼續(xù)吧。
Promise 初見雛形
在上面的 wx.request() 介紹中,實現(xiàn)了 Promise 化,因此對于嵌套回調(diào)場景,可以:
wxRequest('./userInfo')
.then(
data => wxRequest(`./${data.id}/friendList`),
error => {
console.log(error)
}
)
.then(
data => {
console.log(data)
},
error => {
console.log(error)
}
)
通過觀察使用例子,我們來剖析 Promise 的實質(zhì):
結(jié)論 Promise 構(gòu)造函數(shù)返回一個 promise 對象實例,這個返回的 promise 對象具有一個 then 方法。then 方法中,調(diào)用者可以定義兩個參數(shù),分別是 onfulfilled 和 onrejected,它們都是函數(shù)類型。其中 onfulfilled 通過參數(shù),可以獲取 promise 對象 resolved 的值,onrejected 獲得 promise 對象 rejected 的值。通過這個值,我們來處理異步完成后的邏輯。
這些都是規(guī)范的基本內(nèi)容: Promise/A+。
因此,繼續(xù)實現(xiàn)我們的 Promise:
function Promise(executor) {
}
Promise.prototype.then = function(onfulfilled, onrejected) {
}
繼續(xù)復(fù)習(xí) Promise 的知識,看例子來理解:
let promise1 = new Promise((resolve, reject) => {
resolve('data')
})
promise1.then(data => {
console.log(data)
})
let promise2 = new Promise((resolve, reject) => {
reject('error')
})
promise2.then(data => {
console.log(data)
}, error => {
console.log(error)
})
結(jié)論 我們在使用 new 關(guān)鍵字調(diào)用 Promise 構(gòu)造函數(shù)時,在合適的時機(往往是異步結(jié)束時),調(diào)用 executor 的參數(shù) resolve 方法,并將 resolved 的值作為 resolve 函數(shù)參數(shù)執(zhí)行,這個值便可以后續(xù)在 then 方法第一個函數(shù)參數(shù)(onfulfilled)中拿到;同理,在出現(xiàn)錯誤時,調(diào)用 executor 的參數(shù) reject 方法,并將錯誤信息作為 reject 函數(shù)參數(shù)執(zhí)行,這個錯誤信息可以在后續(xù)的 then 方法第二個函數(shù)參數(shù)(onrejected)中拿到。
因此,我們在實現(xiàn) Promise 時,應(yīng)該有兩個值,分別儲存 resolved 的值,以及 rejected 的值(當(dāng)然,因為 Promise 狀態(tài)的唯一性,不可能同時出現(xiàn) resolved 的值和 rejected 的值,因此也可以用一個變量來存儲);同時也需要存在一個狀態(tài),這個狀態(tài)就是 promise 實例的狀態(tài)(pending,fulfilled,rejected);同時還要提供 resolve 方法以及 reject 方法,這兩個方法需要作為 executor 的參數(shù)提供給開發(fā)者使用:
function Promise(executor) {
const self = this
this.status = 'pending'
this.value = null
this.reason = null
function resolve(value) {
self.value = value
}
function reject(reason) {
self.reason = reason
}
executor(resolve, reject)
}
Promise.prototype.then = function(onfulfilled = Function.prototype, onrejected = Function.prototype) {
onfulfilled(this.value)
onrejected(this.reason)
}
為了保證 onfulfilled、onrejected 能夠強健執(zhí)行,我們?yōu)槠湓O(shè)置了默認(rèn)值,其默認(rèn)值為一個函數(shù)元(Function.prototype)。
注意,因為 resolve 的最終調(diào)用是由開發(fā)者在不確定環(huán)境下(往往是在全局中)直接調(diào)用的。為了在 resolve 函數(shù)中能夠拿到 promise 實例的值,我們需要對 this 進(jìn)行保存,上述代碼中用 self 變量記錄 this,或者使用箭頭函數(shù):
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
const resolve = value => {
this.value = value
}
const reject = reason => {
this.reason = reason
}
executor(resolve, reject)
}
Promise.prototype.then = function(onfulfilled = Function.prototype, onrejected = Function.prototype) {
onfulfilled(this.value)
onrejected(this.reason)
}
為什么 then 放在 Promise 構(gòu)造函數(shù)的原型上,而不是放在構(gòu)造函數(shù)內(nèi)部呢?
這涉及到原型、原型鏈的知識了,雖然不是本講的內(nèi)容,這里還是簡單地提一下:每個 promise 實例的 then 方法邏輯是一致的,在實例調(diào)用該方法時,可以通過原型(Promise.prototype)找到,而不需要每次實例化都新創(chuàng)建一個 then 方法,這樣節(jié)省內(nèi)存,顯然更合適。
Promise 實現(xiàn)狀態(tài)完善
我們先來看一到題目,判斷輸出:
let promise = new Promise((resolve, reject) => {
resolve('data')
reject('error')
})
promise.then(data => {
console.log(data)
}, error => {
console.log(error)
})
只會輸出:data,因為我們知道 promise 實例狀態(tài)只能從 pending 改變?yōu)?fulfilled,或者從 pending 改變?yōu)?rejected。狀態(tài)一旦變更完畢,就不可再次變化或者逆轉(zhuǎn)。也就是說:如果一旦變到 fulfilled,就不能再 rejected,一旦變到 rejected,就不能 fulfilled。
而我們的代碼實現(xiàn),顯然無法滿足這一特性。執(zhí)行上一段代碼時,將會輸出 data 以及 error。
因此,需要對狀態(tài)進(jìn)行判斷和完善:
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
const resolve = value => {
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
}
}
const reject = reason => {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
}
}
executor(resolve, reject)
}
Promise.prototype.then = function(onfulfilled, onrejected) {
onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : data => data
onrejected = typeof onrejected === 'function' ? onrejected : error => {throw error}
if (this.status === 'fulfilled') {
onfulfilled(this.value)
}
if (this.status === 'rejected') {
onrejected(this.reason)
}
}
我們看,在 resolve 和 reject 方法中,我們加入判斷,只允許 promise 實例狀態(tài)從 pending 改變?yōu)?fulfilled,或者從 pending 改變?yōu)?rejected。
同時注意,這里我們對 Promise.prototype.then 參數(shù) onfulfilled 和 onrejected 進(jìn)行了判斷,當(dāng)實參不是一個函數(shù)類型時,賦予默認(rèn)函數(shù)值。這時候的默認(rèn)值不再是函數(shù)元 Function.prototype 了。為什么要這么更改?后面會有介紹。
這樣一來,我們的實現(xiàn)顯然更加接近真實了。剛才的例子也可以跑通了:
let promise = new Promise((resolve, reject) => {
resolve('data')
reject('error')
})
promise.then(data => {
console.log(data)
}, error => {
console.log(error)
})
但是不要高興得太早,promise 是解決異步問題的,我們的代碼全部都是同步執(zhí)行的,似乎還差了更重要的邏輯。
Promise 異步完善
到目前為止,實現(xiàn)還差了哪些內(nèi)容呢?別急,我們再從示例代碼分析:
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('data')
}, 2000)
})
promise.then(data => {
console.log(data)
})
正常來講,上述代碼會在 2 秒之后輸出 data,但是我們實現(xiàn)的代碼,并沒有輸入任何信息。這是為什么呢?
原因很簡單,因為我們的實現(xiàn)邏輯全是同步的。在上面實例化一個 promise 的構(gòu)造函數(shù)時,我們是在 setTimeout 邏輯里才調(diào)用 resolve,也就是說,2 秒之后才會調(diào)用 resolve 方法,也才會去更改 promise 實例狀態(tài)。而結(jié)合我們的實現(xiàn),返回實現(xiàn)代碼,then 方法中的 onfulfilled 執(zhí)行是同步的,它在執(zhí)行時 this.status 仍然為 pending,并沒有做到「2 秒中之后再執(zhí)行 onfulfilled」。
那該怎么辦呢?我們似乎應(yīng)該在「合適」的時間才去調(diào)用 onfulfilled 方法,這個合適的時間就應(yīng)該是開發(fā)者調(diào)用 resolve 的時刻,那么我們先在狀態(tài)(status)為 pending 時,把開發(fā)者傳進(jìn)來的 onfulfilled 方法存起來,在 resolve 方法中再去執(zhí)行即可:
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
this.onFulfilledFunc = Function.prototype
this.onRejectedFunc = Function.prototype
const resolve = value => {
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
this.onFulfilledFunc(this.value)
}
}
const reject = reason => {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
this.onRejectedFunc(this.reason)
}
}
executor(resolve, reject)
}
Promise.prototype.then = function(onfulfilled, onrejected) {
onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : data => data
onrejected = typeof onrejected === 'function' ? onrejected : error => {throw error}
if (this.status === 'fulfilled') {
onfulfilled(this.value)
}
if (this.status === 'rejected') {
onrejected(this.reason)
}
if (this.status === 'pending') {
this.onFulfilledFunc = onfulfilled
this.onRejectedFunc = onrejected
}
}
測試一下,發(fā)現(xiàn)現(xiàn)在我們的實現(xiàn)也可以支持異步了!
同時,我們知道 Promise 是異步執(zhí)行的:
let promise = new Promise((resolve, reject) => {
resolve('data')
})
promise.then(data => {
console.log(data)
})
console.log(1)
正常的話,這里會按照順序,輸出 1 再輸出 data。
而我們的實現(xiàn),卻沒有考慮這種情況,先輸出 data 再輸出 1。因此,需要將 resolve 和 reject 的執(zhí)行,放到任務(wù)隊列中。這里姑且先放到 setTimeout 里,保證異步執(zhí)行(這樣的做法并不嚴(yán)謹(jǐn),為了保證 Promise 屬于 microtasks,很多 Promise 的實現(xiàn)庫用了 MutationObserver 來模仿 nextTick)。
const resolve = value => {
if (value instanceof Promise) {
return value.then(resolve, reject)
}
setTimeout(() => {
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
this.onFulfilledFunc(this.value)
}
})
}
const reject = reason => {
setTimeout(() => {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
this.onRejectedFunc(this.reason)
}
})
}
executor(resolve, reject)
這樣一來,在執(zhí)行到 executor(resolve, reject) 時,也能保證在 nextTick 中才去執(zhí)行,不會阻塞同步任務(wù)。
同時我們在 resolve 方法中,加入了對 value 值是一個 Promise 實例的判斷。看一下到目前為止的實現(xiàn)代碼:
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
this.onFulfilledFunc = Function.prototype
this.onRejectedFunc = Function.prototype
const resolve = value => {
if (value instanceof Promise) {
return value.then(resolve, reject)
}
setTimeout(() => {
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
this.onFulfilledFunc(this.value)
}
})
}
const reject = reason => {
setTimeout(() => {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
this.onRejectedFunc(this.reason)
}
})
}
executor(resolve, reject)
}
Promise.prototype.then = function(onfulfilled, onrejected) {
onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : data => data
onrejected = typeof onrejected === 'function' ? onrejected : error => {throw error}
if (this.status === 'fulfilled') {
onfulfilled(this.value)
}
if (this.status === 'rejected') {
onrejected(this.reason)
}
if (this.status === 'pending') {
this.onFulfilledFunc = onfulfilled
this.onRejectedFunc = onrejected
}
}
這樣的實現(xiàn):
et promise = new Promise((resolve, reject) => {
resolve('data')
})
promise.then(data => {
console.log(data)
})
console.log(1)
也會按照順序,輸出 1 再輸出 data。
Promise 細(xì)節(jié)完善
到此為止,似乎我們的 Promise 實現(xiàn)越來越靠譜了,但是還有些細(xì)節(jié)需要完善。
比如當(dāng)我們在 promise 實例狀態(tài)變更之前,添加多個 then 方法:
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('data')
}, 2000)
})
promise.then(data => {
console.log(`1: ${data}`)
})
promise.then(data => {
console.log(`2: ${data}`)
})
應(yīng)該輸出:
1: data
2: data
而我們的實現(xiàn),只會輸出 2: data,這是因為第二個 then 方法中的 onFulfilledFunc 會覆蓋第一個 then 方法中的 onFulfilledFunc。
這個問題也好解決,只需要將所有 then 方法中的 onFulfilledFunc 儲存為一個數(shù)組 onFulfilledArray,在 resolve 時,依次執(zhí)行即可。對于 onRejectedFunc 同理,改動后的實現(xiàn)為:
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
this.onFulfilledArray = []
this.onRejectedArray = []
const resolve = value => {
if (value instanceof Promise) {
return value.then(resolve, reject)
}
setTimeout(() => {
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
this.onFulfilledArray.forEach(func => {
func(value)
})
}
})
}
const reject = reason => {
setTimeout(() => {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
this.onRejectedArray.forEach(func => {
func(reason)
})
}
})
}
executor(resolve, reject)
}
Promise.prototype.then = function(onfulfilled, onrejected) {
onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : data => data
onrejected = typeof onrejected === 'function' ? onrejected : error => {throw error}
if (this.status === 'fulfilled') {
onfulfilled(this.value)
}
if (this.status === 'rejected') {
onrejected(this.reason)
}
if (this.status === 'pending') {
this.onFulfilledArray.push(onfulfilled)
this.onRejectedArray.push(onrejected)
}
}
另外一個細(xì)節(jié),在構(gòu)造函數(shù)中如果出錯,將會自動觸發(fā) promise 實例狀態(tài)為 rejected,我們用 try...catch 塊對 executor 進(jìn)行包裹:
try {
executor(resolve, reject)
} catch(e) {
reject(e)
}
當(dāng)我們故意寫錯時:
let promise = new Promise((resolve, reject) => {
setTout(() => {
resolve('data')
}, 2000)
})
promise.then(data => {
console.log(data)
}, error => {
console.log('got error from promise', error)
})
就可以對錯誤進(jìn)行處理,捕獲到:
got error from promise ReferenceError: setTimeouteout is not defined
at :2:3
at :33:7
at o (web-46c6729d4d8cac92aed8.js:1)
總結(jié)
這一小節(jié),我們已經(jīng)初步實現(xiàn)了基本的 Promise,實現(xiàn)結(jié)果固然重要,但是在實現(xiàn)過程中,也加深了對 Promise 的理解,得出了一些重要結(jié)論:
- Promise 狀態(tài)具有凝固性
- Promise 錯誤處理
- Promise 實例添加多個 then 處理
最后,附上到此為止的全部代碼:
function Promise(executor) {
this.status = 'pending'
this.value = null
this.reason = null
this.onFulfilledArray = []
this.onRejectedArray = []
const resolve = value => {
if (value instanceof Promise) {
return value.then(resolve, reject)
}
setTimeout(() => {
if (this.status === 'pending') {
this.value = value
this.status = 'fulfilled'
this.onFulfilledArray.forEach(func => {
func(value)
})
}
})
}
const reject = reason => {
setTimeout(() => {
if (this.status === 'pending') {
this.reason = reason
this.status = 'rejected'
this.onRejectedArray.forEach(func => {
func(reason)
})
}
})
}
try {
executor(resolve, reject)
} catch(e) {
reject(e)
}
}
Promise.prototype.then = function(onfulfilled, onrejected) {
onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : data => data
onrejected = typeof onrejected === 'function' ? onrejected : error => { throw error}
if (this.status === 'fulfilled') {
onfulfilled(this.value)
}
if (this.status === 'rejected') {
onrejected(this.reason)
}
if (this.status === 'pending') {
this.onFulfilledArray.push(onfulfilled)
this.onRejectedArray.push(onrejected)
}
}
下一講我們將會繼續(xù)實現(xiàn) Promise、處理 Promise 實例的返回問題,以及更多的 Promise 靜態(tài)方法。