手寫一個符合PromiseA+規範的Promise類

前言

在目前的前端開發環境中,Promise的使用愈來愈普遍。今天我就來和你們一塊兒從零開始手寫一個符合PromiseA+規範的Promise類,讓你們在熟悉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的基本特性

不少庫都有本身對Promise的實現,而且在實現原理上會有差距,那爲了兼容它們,就有了PromiseA+規範. 全部的Promise實現都要符合這個規範。編程

特性

new Promise((resolve, reject) => {
    resolve('this is the value')
    reject('this is the reason')
})
複製代碼
  • Promise必須是一個帶有then方法的對象或者函數。
  • Promise的狀態必須是 pending/fulfilled/rejected 之一。
    • 若是是pending狀態,則能夠變爲fulfilled(成功)或者rejected(失敗)。
    • 若是是fulfilled狀態。
      • 不能再變爲其它任何狀態。
      • 必須有一個value(executor裏面resolve的值, resolve(value)),而且這個value是不可變的。
    • 若是是rejected狀態。
      • 不能再變爲其它任何狀態。
      • 必須有一個reason(代碼執行過程當中捕獲到的錯誤, reject(err)),而且這個reason是不可變的。
  • Promise支持鏈式編程,也就是說then方法返回的也會是一個Promise實例。

實現本身的Promise

幫助方法

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函數的第二個參數,用來在執行過程當中發生異常時捕獲異常。
    }
}
複製代碼

實例方法

promise.then()

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
     }
 )
複製代碼
  • then方法接收兩個參數,onFulfilled和onRejected 這兩個參數是可選的但必須是函數,不然會被忽略。(PromiseA+ 2.2.1)
  • 當Promise的狀態變成fulfilled的時候,onFulfilled會被執行,同理,變成rejected的時候,onRejected會被執行。(PromiseA+ 2.2.2/2.2.3)
  • 當執行then方法時Promise的狀態已經不是pending了,onFulfilled和onRejected會被當即執行 (PromiseA+ 2.2.2/2.2.3)
  • Promise的then方法能夠屢次被調用(像我上面的案列代碼)。 (PromiseA+2.2.6)
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的狀態應該怎麼改變呢?

  • 若是onFulfilled或者onRejected返回一個x,那麼須要執行[[Resolve]](promise2, x)。 (PromiseA+ 2.2.7.1)
  • 若是onFulfilled或者onRejected拋出一個異常e,promise2的狀態變爲rejected,而且以e做爲異常的緣由。 (PromiseA+ 2.2.7.2)
  • 若是onFulfilled不是一個函數而且promise1已經fulfilled,那麼promise2也要變爲fulfilled,而且繼承promise1的value。 (PromiseA+ 2.2.7.3)
  • 若是onRejected不是一個函數而且promise1已經rejected,那麼promise2也要變爲rejected,而且繼承promise1的reason。 (PromiseA+ 2.2.7.3)

這裏咱們須要一個幫助方法來幫助咱們處理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執行的結果。

  • 若是x不是一個對象或者函數,直接resolve就可。 (PromiseA+ 2.3.4)
  • 若是x是一個對象或者函數
    • 若是x含有then方法,執行then方法(第一個參數onFulfilled, 第二個onRejected)。(PromiseA+ 2.3.3.3)
      • 若是onFulfilled被以y做爲參數調用了,在onFulfilled內部遞歸執行resolvePromise方法(此處主要是防止y也是一個promise)。 (PromiseA+ 2.3.3.3.1)
      • 若是onRejected被以e做爲參數調用了,直接用相同的reason rejected promise2。 (PromiseA+ 2.3.3.3.2)
      • 若是onFulfilled和onRejected都被調用了,只執行第一個,其餘的忽略掉。這個其實不須要擔憂,由於咱們在以前實現Promise的時候設定狀態是不可逆的,因此在此不須要作任何操做。 (PromiseA+ 2.3.3.3.3)
      • 若是在整個過程當中出現了異常,直接以此異常做爲緣由rejected promise2。 (PromiseA+ 2.3.3.3.4)
    • 若是x沒有then方法,直接resolve就可。 (PromiseA+ 2.3.3.4)

還有一個小問題就是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來測試是否符合規範,具體步驟請查看連接。

promise.catch()

catch方法只是用來捕獲錯誤,也就是then方法的第一個參數爲空。

Promise.prototype.catch = function (onRejected) {
    return this.then(null, onRejected)
}
複製代碼

promise.finally()

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 })
    )
}
複製代碼

靜態方法

Promise.resolve()

resolve方法返回一個成功狀態的Promise

Promise.resolve = function (value) {
    return new Promise(resolve => resolve(value))
}
複製代碼

Promise.reject()

reject方法返回一個失敗狀態的Promise

Promise.reject = function (reason) {
    return new Promise((resolve, reject) => reject(reason))
}
複製代碼

Promise.all()

all方法接收一個數組爲參數。

  • 若是傳入的不是數組,直接將promise以空數組resolve。
  • 首先遍歷數組,若是item不是promise,直接將item做爲最終的結果存在數組相應的位置,若是是promise,等待執行完畢,成功後將值存在數組相應的位置。
  • 必須等全部的promise執行結束後才結束。
  • 只要有一個promise失敗,則整個失敗。
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)
            }

        })
    })
}
複製代碼

Promise.race()

race方法和all方法相同,接收一個數組做爲參數,返回一個新的promise。

  • 若是數組中哪個promise的狀態變爲了成功,則新的promise直接變爲成功,不須要等待其餘的。
  • 只要有一個promise失敗,則新的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))
        })
    })
}
複製代碼

Promise.defer/Promise.deferred

這是一個幫助方法,若是你不喜歡使用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。 若是各位同窗有什麼建議或者問題,歡迎留言討論。

相關文章
相關標籤/搜索