前言
最近總是聽到前端面試要求手寫promise,promise作爲解決前端回調地獄的方案被廣泛運用在前端異步調用上,nodejs更是出了一版返回promise IO接口,所以無論從面試還是實際工作中熟悉promise都是一個合格前端必備技能。
分析Promise
我們知道要實現一個功能,首先要了解需求是什麼,實現Promise也是如此。
那麼Promise到底提供了哪些功能?我們先把Promise中常用的方法列出來
// 使用Promise的標準開頭 new Promise(),可見Promise是一個類
class Promise {
/**
* 常用方式Promise.resolve(something).then(...)
* 可見Promise.resolve返回一個新的Promise實例
**/
static resolve() {}
// 同上
static reject() {}
/**
* 構造函數接收一個函數作爲入參
* 該函數會接收resolve,reject作爲入參,並且立即執行
* Promise將在fn調用resolve/reject後決議
*/
constructor(fn) {}
/**
* 根據constructor我們可以推斷Promise有三個狀態分別是
* pending,fullfilled,rejected
* 我們需要一個內部變量來保存promise的當前狀態
*/
_status = 'pending' // promise的初始狀態爲pending
/**
* 我們需要一個變量保存promise的決議結果
* 考慮 new Promise((resolve) => resolve(2));
**/
_result
/**
* 考慮一下resolve做了什麼事情
* 其實resolve改變promise的狀態並將入參作爲回調函數的入參
**/
resolve(ret) {}
// 同上
reject(ret) {}
/**
* then稱得上是promise的核心方法,到底then做了什麼,我們考慮一下
* then會接收兩個函數,在promise的狀態發生改變後會調用對應的函數
* 所以在這裏then的作用應當是個事件註冊器。
* 需要注意的是then是能多次調用的
* const promise = new Promise(fn)
* promise.then(fn1)
* promise.then(fn2)
* 另外then是支持鏈式調用的,如promise.then(fn3).then(fn4)
* 所以調用then還應當返回一個promise對象
**/
then(fn, fn2) {}
// 描述完then後,我們發現我們需要兩個回調隊列來保存使用then註冊的回調
_successCallback = []
_errorCallback = []
// 我們再定義一個內部方法來異步執行這些回調函數
// 使用入參type區分執行successCallback還是errorCallback
_runCallbackAsync(type) {}
}
實現
分析完Promise的核心功能後,讓我們依次開始實現這些接口。
// 先不看靜態方法和實例方法,從構造器開始實現
class Promise {
...
constructor(fn) {
// 根據之前描述構造器作用主要是運行傳入的fn
// 將fn放在try catch中運行,防止fn執行出錯
try {
//new Promise((resolve, reject) => {})
fn(this.resolve.bind(this), this.reject.bind(this))
} catch(e) {
this.reject(e)
}
}
// resolve調用之後改變後promise的狀態,並且異步執行callback
resolve(result) {
// promise的狀態不是pending,說明該promise已經調用過reject/resolve
if(this._status !== 'pending') throw new Error('重複決議promise')
// 保存決議結果
this._result = result
// 異步執行callback
this._runCallbackAsync('success')
}
// reject也是同理
reject(err) {
if(this._status !== 'pending') throw new Error('重複決議promise')
this._result = err
// 這裏我們執行錯誤回調
this._runCallbackAsync('error')
}
...
}
在寫promise.then之前我們重新考慮下這個方法到底做了什麼?我們逐步實現這個功能點。
- then接收兩個函數,我們暫且稱它爲成功回調和錯誤回調,分別在fullFilled和rejected的時候執行
- 當promise狀態已經是決議狀態時立即執行對應的成功回調/錯誤回調
- 返回一個新的promise,新的promise在成功回調/錯誤回調執行完成後決議,並使用回調的返回作爲決議結果
class Promise {
...
// 省略前後代碼
then(fn1, fn2) {
// 支持鏈式調用,立即回覆一個新的promise對象
return new Promise((resolve, reject) => {
/** 然後我們要將對應的回調函數推入事件隊列,他們將在本個promise
* 決議後由_runCallbackAsync執行。
* 但執行完對應事件後需要將執行結果傳到下一個promise中
* 所以我們需要對回調函數進行小小的處理
*/
// 接收決議結果
const successCallBack = (result) => {
try {
// 將promise的決議結果傳遞給回調函數去執行,並把回調函數的結果作爲下一個promise的決議結果
resolve(fn1(result))
} catch(e) {
// 如果執行出錯,應該將錯誤信息傳遞給promise
reject(e)
}
}
const errorCallback = (e) => {
try {
reject(fn2(e))
} catch(err) {
reject(err)
}
}
// 將回調函數推入事件隊列
if (fn1 && this._status !== 'error') this._successCallback.push(fn1)
if (fn2 && this._status !== 'success') this._errorCallback.push(fn2)
// 如果promise已經決議,則立即執行相應的回調
if(this._status === 'success') this._runCallbackAsync('success')
if(this._status === 'error') this._runCallbackAsync('error')
})
}
// 實現
_runCallbackAsync(type) {
let eventQueue;
if(type === 'error') eventQueue = this._errorCallback;
else eventQueue = this._successCallback;
// 執行回調, 使用settimeout模擬異步執行
setTimeout(() => {
eventQueue.forEach(callback => callback(this._result));
// 清空事件隊列,兩個事件隊列都需要刪除,因爲promise決議後狀態不可變更,決議執行完應當清空所有隊列,以解除引用關係
this._errorCallback.length = 0;
this._successCallback.length = 0;
}, 0)
}
...
}
到此Promise的核心方法已經實現完畢,剩下的方法我們很容易使用現有方法進行實現,例如:
class Promise {
···
catch(fn) {
return this.then(undefined, fn);
}
static resolve(any) {
// 使用鴨子類型去判斷any的類型
if (
typeof any.then === 'function'
typeof any.catch === 'function'
...
) return any;
// 如果不是一個promise則返回一個新的promise
return new Promise((resolve) => {
resolve(any)
})
}
···
}
總結
所以實現promise關鍵點在於,理解promise只決議一次,有三個狀態(pending,fullFilled,rejected),then/catch支持鏈式調用(返回一個新的promise),並且理解then/catch本質是個事件註冊器,類似於then = subscribe('resolve', callback),理解這些自己手動實現一個promise還是不難的。
最後的最後:剛開始寫技術文章,如果有錯漏希望能夠不吝指出,如果覺得寫的還不錯點個贊是對我最大的支持,謝謝。
最後附上源碼鏈接:https://github.com/MinuteWong...