實現一個符合標準的Promise
-- What i can't create, i don't understant
前言
實現Promise的目的是爲了深入的理解Promies,以在項目中遊刃有餘的使用它。完整的代碼見gitHub
Promise標準
完整的Promise/A+標準見這裏,總結如下:
- promise具有狀態state(status),狀態分爲pending, fulfilled(我比較喜歡叫做resolved), rejected。初始爲pending,一旦狀態改變,不能再更改爲其它狀態。當promise爲fulfilled時,具有value;當promise爲rejected時,具有reason;value和reason都是一旦確定,不能改變的。
- promise具有then方法,注意了,只有then方法是必須的,其餘常用的catch,race,all,resolve等等方法都不是必須的,其實這些方法都可以用then方便的實現。
- 不同的promise的實現需要可以相互調用
OK,搞清楚了promise標準之後,開始動手吧
Promise構造函數
產生一個對象有很多種方法,構造函數是看起來最面向對象的一種,而且原生Promise實現也是使用的構造函數,因此我也決定使用構造函數的方法。
首先,先寫一個大概的框架出來:
// 總所周知,Promise傳入一個executor,有兩個參數resolve, reject,用來改變promise的狀態
function Promise(executor) {
this.status = 'pending'
this.value = void 0 // 爲了方便把value和reason合併
const resolve = function() {}
const reject = function() {}
executor(resolve, reject)
}
很明顯,這個構造函數還有很多問題們一個一個來看
-
resolve和reject並沒有什麼卵用。
首先,用過promise的都知道,resolve和reject是用來改變promise的狀態的:function Promise(executor) { this.status = 'pending' this.value = void 0 // 爲了方便把value和reason合併 const resolve = value => { this.value = value this.status = 'resolved' } const reject = reason => { this.value = reason this.status = 'rejected' } executor(resolve, reject) }
然後,當resolve或者reject調用的時候,需要執行在then方法裏傳入的相應的函數(通知)。有沒有覺得這個有點類似於事件(發佈-訂閱模式)呢?
function Promise(executor) { this.status = 'pending' this.value = void 0 // 爲了方便把value和reason合併 this.resolveListeners = [] this.rejectListeners = [] // 通知狀態改變 const notify(target, val) => { target === 'resolved' ? this.resolveListeners.forEach(cb => cb(val)) : this.rejectListeners.forEach(cb => cb(val)) } const resolve = value => { this.value = value this.status = 'resolved' notify('resolved', value) } const reject = reason => { this.value = reason this.status = 'rejected' notify('rejected', reason) } executor(resolve, reject) }
-
status和value並沒有做到一旦確定,無法更改。這裏有兩個問題,一是返回的對象暴露了status和value屬性,並且可以隨意賦值;二是如果在executor裏多次調用resolve或者reject,會使value更改多次。
第一個問題,如何實現只讀屬性:function Promise(executor) { if (typeof executor !== 'function') { throw new Error('Promise executor must be fucntion') } let status = 'pending' // 閉包形成私有屬性 let value = void 0 ...... // 使用status代替this.value const resolve = val => { value = val status = 'resolved' notify('resolved', val) } const reject = reason => { value = reason status = 'rejected' notify('rejected', reason) } // 通過getter和setter設置只讀屬性 Object.defineProperty(this, 'status', { get() { return status }, set() { console.warn('status is read-only') } }) Object.defineProperty(this, 'value', { get() { return value }, set() { console.warn('value is read-only') } })
第二個問題,避免多次調用resolve、reject時改變value,而且標準裏(2.2.2.3 it must not be called more than once)也有規定,then註冊的回調只能執行一次。
const resolve = val => { if (status !== 'pending') return // 避免多次運行 value = val status = 'resolved' notify('resolved', val) }
-
then註冊的回調需要異步執行。
說到異步執行,對原生Promise有了解的同學都知道,then註冊的回調在Micro-task中,並且調度策略是,Macro-task中執行一個任務,清空所有Micro-task的任務。簡而言之,promise異步的優先級更高。
其實,標準只規定了promise回調需要異步執行,在一個“乾淨的”執行棧執行,並沒有規定一定說要用micro-task,並且在低版本瀏覽器中,並沒有micro-task隊列。不過在各種promise的討論中,由於原生Promise的實現,micro-task已經成成爲了事實標準,而且promise回調在micro-task中也使得程序的行爲更好預測。
在瀏覽器端,可以用MutationObserver實現Micro-task。本文利用setTimeout來簡單實現異步。
const resolve = val => { if (val instanceof Promise) { return val.then(resolve, reject) } // 異步執行 setTimeout(() => { if (status !== 'pending') return status = 'resolved' value = val notify('resolved', val) }, 0) }
最後,加上錯誤處理,就得到了一個完整的Promise構造函數:
function Promise(executor) {
if (typeof executor !== 'function') {
throw new Error('Promise executor must be fucntion')
}
let status = 'pending'
let value = void 0
const notify = (target, val) => {
target === 'resolved'
? this.resolveListeners.forEach(cb => cb(val))
: this.rejectListeners.forEach(cb => cb(val))
}
const resolve = val => {
if (val instanceof Promise) {
return val.then(resolve, reject)
}
setTimeout(() => {
if (status !== 'pending') return
status = 'resolved'
value = val
notify('resolved', val)
}, 0)
}
const reject = reason => {
setTimeout(() => {
if (status !== 'pending') return
status = 'rejected'
value = reason
notify('rejected', reason)
}, 0)
}
this.resolveListeners = []
this.rejectListeners = []
Object.defineProperty(this, 'status', {
get() {
return status
},
set() {
console.warn('status is read-only')
}
})
Object.defineProperty(this, 'value', {
get() {
return value
},
set() {
console.warn('value is read-only')
}
})
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
總的來說,Promise構造函數其實只幹了一件事:執行傳入的executor,並構造了executor的兩個參數。
實現then方法
首先需要確定的是,then方法是寫在構造函數裏還是寫在原型裏。
寫在構造函數了裏有一個比較大的好處:可以像處理status和value一樣,通過閉包讓resolveListeners和rejectListeners成爲私有屬性,避免通過this.rejectListeners來改變它。
寫在構造函數裏的缺點是,每一個promise對象都會有一個不同的then方法,這既浪費內存,又不合理。我的選擇是寫在原型裏,爲了保持和原生Promise有一樣的結構和接口。
ok,還是先寫一個大概的框架:
Promise.prototype.then = function (resCb, rejCb) {
this.resolveListeners.push(resCb)
this.rejectListeners.push(rejCb)
return new Promise()
}
隨後,一步一步的完善它:
-
then方法返回的promise需要根據resCb或rejCb的運行結果來確定狀態。
Promise.prototype.then = function (resCb, rejCb) { return new Promise((res, rej) => { this.resolveListeners.push((val) => { try { const x = resCb(val) res(x) // 以resCb的返回值爲value來resolve } catch (e) { rej(e) // 如果出錯,返回的promise以異常爲reason來reject } }) this.rejectListeners.push((val) => { try { const x = rejCb(val) res(x) // 注意這裏也是res而不是rej哦 } catch (e) { rej(e) // 如果出錯,返回的promise以異常爲reason來reject } }) }) }
ps:衆所周知,promise可以鏈式調用,說起鏈式調用,我的第一個想法就是返回this就可以了,但是then方法不可以簡單的返回this,而要返回一個新的promise對象。因爲promise的狀態一旦確定就不能更改,而then方法返回的promise的狀態需要根據then回調的運行結果來決定。
-
如果resCb/rejCb返回一個promiseA,then返回的promise需要跟隨(adopt)promiseA,也就是說,需要保持和promiseA一樣的status和value。
this.resolveListeners.push((val) => { try { const x = resCb(val) if (x instanceof Promise) { x.then(res, rej) // adopt promise x } else { res(x) } } catch (e) { rej(e) } }) this.rejectListeners.push((val) => { try { const x = resCb(val) if (x instanceof Promise) { x.then(res, rej) // adopt promise x } else { res(x) } } catch (e) { rej(e) } })
-
如果then的參數不是函數,需要忽略它,類似於這種情況:
new Promise(rs => rs(5)) .then() .then(console.log)
其實就是把value和狀態往後傳遞
this.resolveListeners.push((val) => { if (typeof resCb !== 'function') { res(val) return } try { const x = resCb(val) if (x instanceof Promise) { x.then(res, rej) // adopt promise x } else { res(x) } } catch (e) { rej(e) } }) // rejectListeners也是相同的邏輯
-
如果調用then時, promise的狀態已經確定,相應的回調直接運行
// 注意這裏需要異步 if (status === 'resolved') setTimeout(() => resolveCb(value), 0) if (status === 'rejected') setTimeout(() => rejectCb(value), 0)
最後,就得到了一個完整的then方法,總結一下,then方法幹了兩件事,一是註冊了回調,二是返回一個新的promise對象。
// resolveCb和rejectCb是相同的邏輯,封裝成一個函數
const thenCallBack = (cb, res, rej, target, val) => {
if (typeof cb !== 'function') {
target === 'resolve'
? res(val)
: rej(val)
return
}
try {
const x = cb(val)
if (x instanceof Promise) {
x.then(res, rej) // adopt promise x
} else {
res(x)
}
} catch (e) {
rej(e)
}
}
Promise.prototype.then = function (resCb, rejCb) {
const status = this.status
const value = this.value
let thenPromise
thenPromise = new Promise((res, rej) => {
/**
* 這裏不能使用bind來實現柯里畫,規範裏規定了:
* 2.2.5: onFulfilled and onRejected must be called as functions (i.e. with no this value))
*/
const resolveCb = val => {
thenCallBack(resCb, res, rej, 'resolve', val)
}
const rejectCb = val => {
thenCallBack(rejCb, res, rej, 'reject', val)
}
if (status === 'pending') {
this.resolveListeners.push(resolveCb)
this.rejectListeners.push(rejectCb)
}
if (status === 'resolved') setTimeout(() => resolveCb(value), 0)
if (status === 'rejected') setTimeout(() => rejectCb(value), 0)
})
return thenPromise
}
不同的Promise實現可以互相調用
首先要明白的是什麼叫互相調用,什麼情況下會互相調用。之前實現then方法的時候,有一條規則是:如果then方法的回調返回一個promiseA。then返回的promise需要adopt這個promiseA,也就是說,需要處理這種情況:
new MyPromise(rs => rs(5))
.then(val => {
return Promise.resolve(5) // 原生Promise
})
.then(val => {
return new Bluebird(r => r(5)) // Bluebird的promise
})
關於這個,規範裏定義了一個叫做The Promise Resolution Procedure的過程,我們需要做的就是把規範翻譯一遍,並替代代碼中判斷promise的地方
const resolveThenable = (promise, x, resolve, reject) => {
if (x === promise) {
return reject(new TypeError('chain call found'))
}
if (x instanceof Promise) {
return x.then(v => {
resolveThenable(promise, v, resolve, reject)
}, reject)
}
if (x === null || (typeof x !== 'object' && typeof x !== 'function')) {
return resolve(x)
}
let called = false
try {
// 這裏有一個有意思的技巧。標準裏解釋了,如果then是一個getter,那麼通過賦值可以保證getter只被觸發一次,避免副作用
const then = x.then
if (typeof then !== 'function') {
return resolve(x)
}
then.call(x, v => {
if (called) return
called = true
resolveThenable(promise, v, resolve, reject)
}, r => {
if (called) return
called = true
reject(r)
})
} catch (e) {
if (called) return
reject(e)
}
}
到這裏,一個符合標準的Promise就完成了,完整的代碼如下:
function Promise(executor) {
if (typeof executor !== 'function') {
throw new Error('Promise executor must be fucntion')
}
let status = 'pending'
let value = void 0
const notify = (target, val) => {
target === 'resolved'
? this.resolveListeners.forEach(cb => cb(val))
: this.rejectListeners.forEach(cb => cb(val))
}
const resolve = val => {
if (val instanceof Promise) {
return val.then(resolve, reject)
}
setTimeout(() => {
if (status !== 'pending') return
status = 'resolved'
value = val
notify('resolved', val)
}, 0)
}
const reject = reason => {
setTimeout(() => {
if (status !== 'pending') return
status = 'rejected'
value = reason
notify('rejected', reason)
}, 0)
}
this.resolveListeners = []
this.rejectListeners = []
Object.defineProperty(this, 'status', {
get() {
return status
},
set() {
console.warn('status is read-only')
}
})
Object.defineProperty(this, 'value', {
get() {
return value
},
set() {
console.warn('value is read-only')
}
})
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
const thenCallBack = (cb, res, rej, target, promise, val) => {
if (typeof cb !== 'function') {
target === 'resolve'
? res(val)
: rej(val)
return
}
try {
const x = cb(val)
resolveThenable(promise, x, res, rej)
} catch (e) {
rej(e)
}
}
const resolveThenable = (promise, x, resolve, reject) => {
if (x === promise) {
return reject(new TypeError('chain call found'))
}
if (x instanceof Promise) {
return x.then(v => {
resolveThenable(promise, v, resolve, reject)
}, reject)
}
if (x === null || (typeof x !== 'object' && typeof x !== 'function')) {
return resolve(x)
}
let called = false
try {
// 這裏有一個有意思的技巧。標準裏解釋了,如果then是一個getter,那麼通過賦值可以保證getter只被觸發一次,避免副作用
const then = x.then
if (typeof then !== 'function') {
return resolve(x)
}
then.call(x, v => {
if (called) return
called = true
resolveThenable(promise, v, resolve, reject)
}, r => {
if (called) return
called = true
reject(r)
})
} catch (e) {
if (called) return
reject(e)
}
}
Promise.prototype.then = function (resCb, rejCb) {
const status = this.status
const value = this.value
let thenPromise
thenPromise = new Promise((res, rej) => {
const resolveCb = val => {
thenCallBack(resCb, res, rej, 'resolve', thenPromise, val)
}
const rejectCb = val => {
thenCallBack(rejCb, res, rej, 'reject', thenPromise, val)
}
if (status === 'pending') {
this.resolveListeners.push(resolveCb)
this.rejectListeners.push(rejectCb)
}
if (status === 'resolved') setTimeout(() => resolveCb(value), 0)
if (status === 'rejected') setTimeout(() => rejectCb(value), 0)
})
return thenPromise
}
關於promise的一些零散知識
- Promise.resolve就是本文所實現的resolveThenable,並不是簡單的用來返回一個resolved狀態的函數,它返回的promise對象的狀態也並不一定是resolved。
- promise.then(rs, rj)和promise.then(rs).catch(rj)是有區別的,區別在於當rs出錯時,後一種方法可以進行錯誤處理。
感想與總結
實現Promise的過程其實並沒有我預想的那麼難,所謂的Promise的原理我感覺就是類似於觀察者模式,so,不要有畏難情緒,我上我也行^_^。