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