在目前的前端開發環境中,Promise的使用愈來愈普遍。今天我就來和你們一塊兒從零開始手寫一個符合PromiseA+規範的Promise類,讓你們在熟悉Promise使用的同時,可以瞭解它的實現原理。前端
在Promise沒有出現以前,咱們在解決異步問題的時候,使用的最多的就是回調函數。好比$.ajax:git
$.ajax({
...
success: function(res){
// success callback
}
})
複製代碼
假設一種場景,一個Http請求要在另外一個的基礎上發出,咱們就得這樣寫:github
$.ajax({
...
success: function(res){
$.ajax({
...
success: function(res){
// success callback
}
})
}
})
複製代碼
若是有更多層的嵌套的話,咱們的代碼就會寫成一個死亡嵌套:ajax
$.ajax({
success: function(res){
$.ajax({
success: function(res){
$.ajax({
success: function(res){
...
}
})
}
})
}
})
複製代碼
這樣的代碼首先在寫法上就很不優雅,讓人頭暈目眩。在這種狀況下,Promise應運而生。它就是異步編程的一種解決方案,支持鏈式編程,咱們不再須要多層回調嵌套來實現異步代碼的編寫。npm
不少庫都有本身對Promise的實現,而且在實現原理上會有差距,那爲了兼容它們,就有了PromiseA+規範. 全部的Promise實現都要符合這個規範。編程
new Promise((resolve, reject) => {
resolve('this is the value')
reject('this is the reason')
})
複製代碼
resolve(value)
),而且這個value是不可變的。reject(err)
),而且這個reason是不可變的。const isArray = validateType('Array')
const isObject = validateType('Object')
const isFunction = validateType('Function')
const PROMISE_STATUS = Object.freeze({
PENDING: 'PENDING',
FULFILLED: 'FULFILLED',
REJECTED: 'REJECTED'
})
function validateType(type) {
return function (source) {
return Object.prototype.toString.call(source) === `[object ${type}]`
}
}
function isPromise(source) {
return source && isObject(source) && isFunction(source.then)
}
複製代碼
先來定義咱們須要的幾個屬性和方法數組
function Promise(executor){
this.value = undefined // 存儲成功的值
this.reason = undefined // 存儲失敗時拋出的異常信息
this.status = PROMISE_STATUS.PENDING // Promise的狀態
// 分別存儲成功和失敗的回調函數,由於then是能夠被屢次調用的,也就是說可能會有多個回調函數,因此這裏用數組來存儲
this.fulfilledCallbacks = []
this.rejectedCallbacks = []
// ------------------------------------------------------------------------------------------------------
function resolve(value) {
// executor函數的第一個參數,用來執行成功後傳遞value。
}
function reject(reason) {
// executor函數的第二個參數,用來在執行過程當中發生異常時捕獲異常。
}
}
複製代碼
接下來咱們須要在構造器裏面執行咱們傳遞進來的執行函數,若是在執行過程當中有異常拋出,直接使用reject捕獲。promise
function Promise(executor){
this.value = undefined // 存儲成功的值
this.reason = undefined // 存儲失敗時拋出的異常信息
this.status = PROMISE_STATUS.PENDING // Promise的狀態
// 分別存儲成功和失敗的回調函數,由於then是能夠被屢次調用的,也就是說可能會有多個回調函數,因此這裏用數組來存儲
this.fulfilledCallbacks = []
this.rejectedCallbacks = []
// 執行傳遞進來的executor函數,若是在執行過程當中有異常拋出,直接使用reject捕獲。
try {
executor(resolve.bind(this), reject.bind(this))
} catch (err) {
reject(err)
}
// ------------------------------------------------------------------------------------------------------
function resolve(value) {
// executor函數的第一個參數,用來執行成功後傳遞value。
}
function reject(reason) {
// executor函數的第二個參數,用來在執行過程當中發生異常時捕獲異常。
}
}
複製代碼
const p = new Promise((resolve, reject) => {
// Using setTimeout to simulate async code.
setTimeout(() => resolve('success'), 1000)
})
p.then(
value => {
// onFulfilled callback
console.log(value) // success
},
reason => {
// onRejected callback
}
)
p.then(
value => {
// onFulfilled callback
console.log(value) // success
},
reason => {
// onRejected callback
}
)
複製代碼
Promise.prototype.then = function(onFulfilled, onRejected) {
const { status, value, reason } = this
switch(status){
case PROMISE_STATUS.FULFILLED:
onFulfilled(value)
break
case PROMISE_STATUS.FULFILLED:
onRejected(value)
break
case PROMISE_STATUS.PENDING:
this.rejectedCallbacks.push(onRejected)
this.fulfilledCallbacks.push(onFulfilled)
}
}
複製代碼
若是是fulfilled或者rejected狀態,直接執行回調函數,若是是pending狀態,將回調函數入棧,等待狀態改變以後再依次執行。bash
那到底狀態是何時改變的,咱們怎麼知道的呢? 來看一段代碼異步
const p = new Promise((resolve, reject) => {
// Using setTimeout to simulate async code.
setTimeout(() => resolve('success'), 1000)
})
複製代碼
在executor裏面咱們能夠獲得兩個參數,一個resolve,另外一個reject。 若是執行成功了就調用resolve,失敗了就調用reject。因此只要用戶調用了resolve或者reject,那就代表Promise的狀態發生了改變。因此咱們回去實現一下構造器裏面的resolve和reject方法。
function resolve(value) {
// 若是resolve的是一個promise,咱們就遞歸執行直到resolve的不是一個promise
if (value instanceof Promise) return value.then(resolve.bind(this), reject.bind(this))
// 由於Promise的狀態是不可逆的,因此一旦狀態變成了fulfilled或者rejected,就不會再有任何變化。
if (this.status !== PROMISE_STATUS.PENDING) return
this.value = value
this.status = PROMISE_STATUS.FULFILLED
// 此時狀態已經更新爲fulfilled,循環執行全部的回調。
this.fulfilledCallbacks.forEach(fulfilledCallback => fulfilledCallback(value))
}
function reject(reason) {
if (this.status !== PROMISE_STATUS.PENDING) return
this.reason = reason
this.status = PROMISE_STATUS.REJECTED
this.rejectedCallbacks.forEach(rejectedCallback => rejectedCallback(reason))
}
複製代碼
到這裏一個簡單的Promise已經實現了,不過還有一個問題,就是上面提到的Promise是支持鏈式調用的:
這就意味着then方法返回的應該也是一個Promise (PromiseA+ 3.3):
Promise.prototype.then = function(onFulfilled, onRejected) {
const { status, value, reason } = this
let promise2 = new Promise((resolve, reject) => {
switch(status){
case PROMISE_STATUS.FULFILLED:
onFulfilled(value)
break
case PROMISE_STATUS.FULFILLED:
onRejected(value)
break
case PROMISE_STATUS.PENDING:
this.rejectedCallbacks.push(onRejected)
this.fulfilledCallbacks.push(onFulfilled)
}
})
return promise2
}
複製代碼
promise2 = promise1.then(onFulfilled, onRejected)
複製代碼
那麼promise2的狀態應該怎麼改變呢?
[[Resolve]](promise2, x)
。 (PromiseA+ 2.2.7.1)這裏咱們須要一個幫助方法來幫助咱們處理promise2的狀態。
function resolvePromise(promise2, x, resolve, reject) {
if (x && (isObject(x) || isFunction(x))) {
try {
let then = x.then
if (isFunction(then)) {
then.call(
x,
y => {
resolvePromise(promise2, y, resolve, reject)
},
r => {
reject(r)
}
)
} else {
resolve(x)
}
} catch (err) {
reject(err)
}
} else {
resolve(x)
}
}
複製代碼
此處的x是onFulfilled或者onRejected執行的結果。
還有一個小問題就是promise2和x不能是同一個,若是是同一個就會報錯。 (PromiseA+ 2.3.1)
因此咱們再多作一個小處理
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
...
}
複製代碼
完成了這個輔助方法,咱們再返回來完善一下咱們的then方法。
Promise.prototype.then = function (onFulfilled, onRejected) {
onFulfilled = isFunction(onFulfilled) ? onFulfilled : data => data
onRejected = isFunction(onRejected) ? onRejected : err => { throw err }
const { status, value, reason } = this
let promise2 = new Promise((resolve, reject) => {
switch (status) {
case PROMISE_STATUS.FULFILLED:
runResolvePromise(promise2, onFulfilled(value), resolve, reject)
break
case PROMISE_STATUS.REJECTED:
runResolvePromise(promise2, onRejected(reason), resolve, reject)
break
case PROMISE_STATUS.PENDING:
this.rejectedCallbacks.push( reason => runResolvePromise(promise2, onRejected(reason), resolve, reject))
this.fulfilledCallbacks.push( value => runResolvePromise(promise2, onFulfilled(value), resolve, reject))
}
})
return promise2
}
複製代碼
上面咱們說過, 若是在執行onFulfilled或者onRejected拋出一個異常e,promise2的狀態變爲rejected,而且以e做爲異常的緣由。 (PromiseA+ 2.2.7.2), 因此咱們須要對他們的執行作tryCatch的處理。由於他們執行了屢次,因此咱們這裏寫一個幫助方法來一次性捕獲錯誤,這樣不須要寫多個tryCatch。
function runResolvePromiseWithErrorCapture(promise, onFulfilledOrOnRejected, resolve, reject, valueOrReason) {
try {
let x = onFulfilledOrOnRejected(valueOrReason)
resolvePromise(promise, x, resolve, reject)
} catch (e) {
reject(e)
}
}
複製代碼
最終的then方法是這樣子的:
Promise.prototype.then = function (onFulfilled, onRejected) {
onFulfilled = isFunction(onFulfilled) ? onFulfilled : data => data
onRejected = isFunction(onRejected) ? onRejected : err => { throw err }
const { status, value, reason } = this
let promise2 = new Promise((resolve, reject) => {
switch (status) {
case PROMISE_STATUS.FULFILLED:
setTimeout(() => {
runResolvePromiseWithErrorCapture(promise2, onFulfilled, resolve, reject, this.value)
}, 0)
break
case PROMISE_STATUS.REJECTED:
setTimeout(() => {
runResolvePromiseWithErrorCapture(promise2, onRejected, resolve, reject, this.reason)
}, 0)
break
case PROMISE_STATUS.PENDING:
this.rejectedCallbacks.push(reason => runResolvePromiseWithErrorCapture(promise2, onRejected, resolve, reject, reason))
this.fulfilledCallbacks.push(value => runResolvePromiseWithErrorCapture(promise2, onFulfilled, resolve, reject, value))
}
})
return promise2
}
複製代碼
使用setTimeout是由於此處的runResolvePromiseWithErrorCapture是當即執行的,可是onFulfilled(value)的執行多是異步的,所以咱們拿不到promise2。這裏藉助setTimeout來延遲執行。
寫到這裏其實一個符合PromiseA+規範的類已經實現了,咱們可使用promises-aplus-tests來測試是否符合規範,具體步驟請查看連接。
catch方法只是用來捕獲錯誤,也就是then方法的第一個參數爲空。
Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected)
}
複製代碼
finally方法是在不論promise成功或者失敗都會被調用的方法,好比咱們有一個Http請求,請求以前咱們打開了一個loading,那麼不論是這個請求成功或者失敗,在請求結束以後咱們都要關閉這個loading,finally就能夠用來作這個事情。
Promise.prototype.finally = function (callback) {
return this.then(
value => Promise.resolve(callback()).then(() => value),
err => Promise.resolve(callback()).then(() => { throw err })
)
}
複製代碼
resolve方法返回一個成功狀態的Promise
Promise.resolve = function (value) {
return new Promise(resolve => resolve(value))
}
複製代碼
reject方法返回一個失敗狀態的Promise
Promise.reject = function (reason) {
return new Promise((resolve, reject) => reject(reason))
}
複製代碼
all方法接收一個數組爲參數。
Promise.all = function (promises) {
promises = isArray(promises) ? promises : []
let fulfilledCount = 0
let promisesLength = promises.length
let results = new Array(promisesLength)
return new Promise((resolve, reject) => {
if (promisesLength === 0) return resolve([])
promises.forEach((promise, index) => {
if (isPromise(promise)) {
promise.then(
value => {
results[index] = value
if (++fulfilledCount === promisesLength) resolve(results)
},
err => reject(err)
)
} else {
results[index] = promise
if (++fulfilledCount === promisesLength) resolve(results)
}
})
})
}
複製代碼
race方法和all方法相同,接收一個數組做爲參數,返回一個新的promise。
Promise.race = function (promises) {
promises = isArray(promises) ? promises.filter(isPromise) : []
return new Promise((resolve, reject) => {
promises.forEach(promise => {
promise.then(value => resolve(value), err => reject(err))
})
})
}
複製代碼
這是一個幫助方法,若是你不喜歡使用new關鍵字來寫,可使用這個方法。
Promise.defer = Promise.deferred = function () {
let dfd = {}
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}
const p1 = Promise.defer()
fetch(url).then(res => {
p1.resolve(res)
})
p1.promise.then(res => {
// do something
}
)
複製代碼
完整的源碼在個人github promise。 若是各位同窗有什麼建議或者問題,歡迎留言討論。