完美通過測試的Promise/A+規範源碼分析

Promise/A+ 規範,源碼分析

GitHub
promiseA.jpg

Promise是前端大廠面試的一道常考題,掌握Promise用法及其相關原理,對你的面試一定有很大幫助。這篇文章主要講解Promise源碼實現,如果你還沒有掌握Promise的功能和API,推薦你先去學習一下Promise的概念和使用API,學習知識就要腳踏實地,先把基礎搞好才能深刻理解源碼的實現。
這裏推薦阮一峯老師的文章

ES6入門-Promise對象

如果你已經掌握了Promise的基本用法,我們進行下一步

Promise/A+規範

說到Promise/A+規範,很多同學可能很不理解這是一個什麼東西,下面給出兩個地址,不瞭解的同學需要先了解一下,對我們後續理解源碼很有幫助,先看兩遍,有些地方看不懂也沒關係,後續我們可以通過源碼來回頭再理解,想把一個知識真的學會,就要反覆琢磨,從【肯定->否定->再肯定】不斷地深入理解,直到完全掌握。

Promise/A+規範英文地址
Promise/A+規範中文翻譯

如果你看過了Promise/A+規範,我們繼續,我會帶着大家按照規範要求,一步一步的來實現源碼

Promise/A+ 【2.1】

2.1Promise狀態

一個promise必須處於三種狀態之一: 請求態(pending), 完成態(fulfilled),拒絕態(rejected)

2.1.1 當promise處於請求狀態(pending)時
  • 2.1.1.1 promise可以轉爲fulfilled或rejected狀態
2.1.2 當promise處於完成狀態(fulfilled)時
  • 2.1.2.1 promise不能轉爲任何其他狀態

2.1.2.2 必須有一個值,且此值不能改變

2.1.3 當promise處於拒絕狀態(rejected)時
  • 2.1.3.1 promise不能轉爲任何其他狀態
  • 2.1.3.2 必須有一個原因(reason),且此原因不能改變

我們先找需求來完成這一部分代碼,一個簡單的小架子

    // 2.1 狀態常量
    const PENDING = 'penfing';
    const RESOLVED = 'resolved';
    const REJECTED = 'rejected';
    
    // Promise構造函數
    function MyPromise(fn) {
        const that = this;
        this.state = PENDING;
        this.value = null;
        this.resolvedCallbacks = [];
        this.rejectedCallbacks = [];
        function resolve() {
            if (that.state === PENDING) {
    
            }
        }
        function reject() {
            if (that.state === PENDING) {
                
            }
        }
    }

上面這段代碼完成了Promise構造函數的初步搭建,包含:

  • 三個狀態的常量聲明【請求態、完成態、拒絕態】
  • this.state保管狀態、this.value保存唯一值
  • resolvedCallbacks 和 rejectedCallbacks 用於保存 then 中的回調,因爲當執行完 Promise 時狀態可能還是等待中,這時候應該把 then 中的回調保存起來用於狀態改變時使用
  • 給fn的回調函數 reslove、reject
  • resolve、reject確保只有'pedding'狀態纔可以改變狀態

下面我們來完成resolve和reject

    function resolve(value) {
        if (that.state === PENDING) {
            that.state = RESOLVED
            that.value = value
            that.resolvedCallbacks.map(cb => cb(that.value))
        }
    }

    function reject(value) {
        if (that.state === PENDING) {
            that.state = REJECTED
            that.value = value
            that.rejectedCallbacks.map(cb => cb(that.value))
        }
    }
    
  • 更改this.state的狀態
  • 給this.value賦值
  • 遍歷回調數組並執行,傳入this.value

記下來我們需要來執行新建Promise傳入的函數體

        try {
            fn(resolve, reject);
        } catch (e){
            reject(e)
        }

在執行過程中可能會遇到錯誤,需要捕獲錯誤傳給reject

Promise/A+ 【2.2】

2.2 then方法

promise必須提供then方法來存取它當前或最終的值或者原因。
promise的then方法接收兩個參數:

    promise.then(onFulfilled, onRejected)
2.2.1 onFulfilled和onRejected都是可選的參數:
  • 2.2.1.1 如果 onFulfilled不是函數,必須忽略
  • 2.2.1.1 如果 onRejected不是函數,必須忽略
2.2.2 如果onFulfilled是函數:
  • 2.2.2.1 此函數必須在promise 完成(fulfilled)後被調用,並把promise 的值作爲它的第一個參數
  • 2.2.2.2 此函數在promise完成(fulfilled)之前絕對不能被調用
  • 2.2.2.2 此函數絕對不能被調用超過一次
2.2.3 如果onRejected是函數:
  • 2.2.3.1 此函數必須在promise rejected後被調用,並把promise 的reason作爲它的第一個參數
  • 2.2.3.2 此函數在promise rejected之前絕對不能被調用
  • 2.2.3.2 此函數絕對不能被調用超過一次

現根據這些要求我們先實現個簡單的then函數:

    MyPromise.prototype.then = function (onFulfilled, onRejected) {
        const that = this
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
        onRejected =
            typeof onRejected === 'function'
                ? onRejected
                : r => {
                    throw r
                }
        if (that.state === PENDING) {
            that.resolvedCallbacks.push(onFulfilled)
            that.rejectedCallbacks.push(onRejected)
        }
        if (that.state === RESOLVED) {
            onFulfilled(that.value)
        }
        if (that.state === REJECTED) {
            onRejected(that.value)
        }
    }
  • 首先判斷了傳進來的onFulfilled和onRejected是不是一個函數類型,如果不是就創建一個透傳數據的函數
  • 判斷狀態,如果是'pending'就把函數追加到對應的隊列中,如果不是'pending',直接執行對應狀態的函數【resolves => onFulfilled, rejected => onRejected】

如上我們就完成了一個簡易版的promise,但是還不能完全滿足Promise/A+規範,接下來我們繼續完善

2.2.4 在執行上下文堆棧(execution context)僅包含平臺代碼之前,不得調用 onFulfilled和onRejected
2.2.5 onFulfilled and onRejected must be called as functions (i.e. with no this value)
2.2.6 then可以在同一個promise裏被多次調用

— 2.2.6.1 如果/當 promise 完成執行(fulfilled),各個相應的onFulfilled回調 必須根據最原始的then 順序來調用
— 2.2.6.2 如果/當 promise 被拒絕(rejected),各個相應的onRejected回調 必須根據最原始的then 順序來調用

2.2.7 then必須返回一個promise
promise2 = promise1.then(onFulfilled, onRejected);
  • 2.2.7.1 如果onFulfilled或onRejected返回一個值x, 運行 Promise Resolution Procedure [[Resolve]](promise2, x)
  • 2.2.7.2 如果onFulfilled或onRejected拋出一個異常e,promise2 必須被拒絕(rejected)並把e當作原因
  • 2.2.7.3 如果onFulfilled不是一個方法,並且promise1已經完成(fulfilled), promise2必須使用與promise1相同的值來完成(fulfiled)
  • 2.2.7.4 如果onRejected不是一個方法,並且promise1已經被拒絕(rejected), promise2必須使用與promise1相同的原因來拒絕(rejected)

首先我們先把resolve和rejected完善一下

    function resolve(value) {
        if (value instanceof MyPromise) {
            return value.then(resolve, reject)
        }
        setTimeout(() => {
            if (that.state === PENDING) {
                that.state = RESOLVED
                that.value = value
                that.resolvedCallbacks.map(cb => cb(that.value))
            }
        }, 0)
    }
    function reject(value) {
        setTimeout(() => {
            if (that.state === PENDING) {
                that.state = REJECTED
                that.value = value
                that.rejectedCallbacks.map(cb => cb(that.value))
            }
        }, 0)
    }

參考2.2.2和2.2.3

  • 對於 resolve 函數來說,首先需要判斷傳入的值是否爲 Promise 類型
  • 爲了保證函數執行順序,需要將兩個函數體代碼使用 setTimeout 包裹起來

接下來根據規範需求繼續完善then函數裏的代碼:

    if (that.state === PENDING) {
    
        return (promise2 = new MyPromise((resolve, reject) => {
            that.resolvedCallbacks.push(() => {
                try {
                    const x = onFulfilled(that.value);
                    resoluteProcedure(promise2, x, resolve, reject)
                } catch (r) {
                    reject(r);
                }
            });
            that.rejectedCallbacks.push(() => {
                try {
                    const x = onRejected(that.value);
                    resoluteProcedure(promise2, x, resolve, reject)
                } catch {
                    reject(r)
                }
            })
        }));
        that.reolvedCallbacks.push(onFulfilled);
        that.rejectedCallbacks.push(onRejeted);
    }
    if (that.state === RESOLVED) {
        return (promise2 = new MyPromise((resolve, reject) => {
            setTimeout(() => {
                try {
                    const x = onFulfilled(that.value)
                    resolutionProcedure(promise2, x, resolve, reject)
                } catch (reason) {
                    reject(reason)
                }
            })
        }))
    }
    if (that.state === REJECTED) {
        return (promise2 = new MyPromise((resolve, reject) => {
            setTimeout(() => {
                try {
                    const x = onRejected(that.value)
                    resolutionProcedure(promise2, x, resolve, reject)
                } catch (reason) {
                    reject(reason)
                }
            })
        }))
    }
  • 首先我們返回了一個新的 Promise 對象,並在 Promise 中傳入了一個函數
  • 函數的基本邏輯還是和之前一樣,往回調數組中 push 函數
  • 同樣,在執行函數的過程中可能會遇到錯誤,所以使用了 try...catch 包裹
  • 規範規定,執行 onFulfilled 或者 onRejected 函數時會返回一個 x,並且執行 Promise 解決過程,這是爲了不同的 Promise 都可以兼容使用,比如 JQuery 的 Promise 能兼容 ES6 的 Promise

Promise/A+ 【2.3】

2.3 Promise解決程序

2.3.1 如果promise和x引用同一個對象,則用TypeError作爲原因拒絕(reject)promise。
2.3.2 如果x是一個promise,採用promise的狀態
  • 2.3.2.1 如果x是請求狀態(pending),promise必須保持pending直到xfulfilled或rejected
  • 2.3.2.2 如果x是完成態(fulfilled),用相同的值完成fulfillpromise
  • 2.3.2.2 如果x是拒絕態(rejected),用相同的原因rejectpromise
2.3.3另外,如果x是個對象或者方法
  • 2.3.3.1 讓x作爲x.then
  • 2.3.3.2 如果取回的x.then屬性的結果爲一個異常e,用e作爲原因reject promise
  • 2.3.3.3 如果then是一個方法,把x當作this來調用它, 第一個參數爲 resolvePromise,第二個參數爲rejectPromise,其中:

    • 2.3.3.3.1 如果/當 resolvePromise被一個值y調用,運行 [[Resolve]](promise, y)
    • 2.3.3.3.2 如果/當 rejectPromise被一個原因r調用,用r拒絕(reject)promise
    • 2.3.3.3.3 如果resolvePromise和 rejectPromise都被調用,或者對同一個參數進行多次調用,第一次調用執行,任何進一步的調用都被忽略
    • 2.3.3.3.4 如果調用then拋出一個異常e,

      • 2.3.3.3.4.1 如果resolvePromise或 rejectPromise已被調用,忽略。
      • 2.3.3.3.4.2 或者, 用e作爲reason拒絕(reject)promise
  • 2.3.3.4 如果then不是一個函數,用x完成(fulfill)promise
2.3.4 如果 x既不是對象也不是函數,用x完成(fulfill)promise

如果一個promise被一個thenable resolve,並且這個thenable參與了循環的thenable環,
[[Resolve]](promise, thenable)的遞歸特性最終會引起[[Resolve]](promise, thenable)再次被調用。
遵循上述算法會導致無限遞歸,鼓勵(但不是必須)實現檢測這種遞歸併用包含信息的TypeError作爲reason拒絕(reject)
這部分規範主要描述了resolutionProcedure函數的規範,下面我們來實現resolutionProcedure這個函數,我先我麼你關注2.3.4下面那段話,簡單的來說規定了x不能與promise2相等,這樣會發生循環引用的問題,如下栗子:

    let p = new MyPromise((resolve, reject) => {
        resolve(1)
    })
    let p1 = p.then(value => {
        return p1
    })

所以我們需要先進行檢測,代碼如下:

    function resolutionProcedure(promise2, x, resolve, reject) {
        if (promise2 === x) {
            return reject(new TypeError('Error'))
        }
    }

接下來我們判斷x的類型

    if (x instanceof MyPromise) {
        x.then(function (value) {
            resolutionProcedure(promise2, value, resolve, reject)
        }, reject)
    }

如果 x 爲 Promise 的話,需要判斷以下幾個情況:

  • 如果 x 處於等待態,Promise 需保持爲等待態直至 x 被執行或拒絕
  • 如果 x 處於其他狀態,則用相同的值處理 Promise

最後我們來完成剩餘的代碼:

    let called = false
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        try {
            let then = x.then
            if (typeof then === 'function') {
                then.call(
                    x,
                    y => {
                        if (called) return
                        called = true
                        resolutionProcedure(promise2, y, resolve, reject)
                    },
                    e => {
                        if (called) return
                        called = true
                        reject(e)
                    }
                )
            } else {
                resolve(x)
            }
        } catch (e) {
            if (called) return
            called = true
            reject(e)
        }
    } else {
        resolve(x)
    }
  • 首先創建一個變量 called 用於判斷是否已經調用過函數
  • 然後判斷 x 是否爲對象或者函數,如果都不是的話,將 x 傳入 resolve 中
  • 如果 x 是對象或者函數的話,先把 x.then 賦值給 then,然後判斷 then 的類型,如果不是函數類型的話,就將 x 傳入 resolve 中
  • 如果 then 是函數類型的話,就將 x 作爲函數的作用域 this 調用之,並且傳遞兩個回調函數作爲參數,第一個參數叫做 resolvePromise ,第二個參數叫做 rejectPromise,兩個回調函數都需要判斷是否已經執行過函數,然後進行相應的邏輯
  • 以上代碼在執行的過程中如果拋錯了,將錯誤傳入 reject 函數中

測試Promise

有專門的測試腳本可以測試所編寫的代碼是否符合PromiseA+的規範
首先,在promise實現的代碼中,增加以下代碼:

    Promise.defer = Promise.deferred = function () {
        let dfd = {};
        dfd.promise = new Promise((resolve, reject) => {
            dfd.resolve = resolve;
            dfd.reject = reject;
        });
        return dfd;
    }

安裝測試腳本:

npm install -g promises-aplus-tests

如果當前的promise源碼的文件名爲promise.js

那麼在對應的目錄執行以下命令:

promises-aplus-tests promise.js

共有872條測試用例,可以完美通過
promise_test.jpg

符合Promise/A+規範完整代碼

這樣我們就完成了符合Promise/A+規範的源碼,下面是整個代碼:

    const PENDING = 'pending';
    const RESOLVED = 'resolve';
    const REJECTED = 'rejected';
    function Promise(fn) {
        let that = this;
        that.status = 'PENDING';
        that.value = undefined;
        that.resolvedCallbacks = [];
        that.rejectedCallbacks = [];
        function resolve(value) {
            if (that.status === 'PENDING') {
                that.status = 'RESOLVED';
                that.value = value;
                that.resolvedCallbacks.forEach(function (fn) {
                    fn();
                })
            }
        }
        function reject(value) {
            if (that.status === 'PENDING') {
                that.status = 'REJECTED';
                that.value = value;
                that.rejectedCallbacks.forEach(function (fn) {
                    fn();
                })
            }
        }
        try {
            fn(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }

    function resolutionProcedure(promise2, x, resolve, reject) {
        //有可能這裏返回的x是別人的promise 要儘可能允許其他人亂寫 
        if (promise2 === x) {//這裏應該報一個循環引用的類型錯誤
            return reject(new TypeError('循環引用'));
        }
        //看x是不是一個promise promise應該是一個對象
        let called;  //表示是否調用過成功或者失敗
        if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
            //可能是promise 看這個對象中是否有then 如果有 姑且作爲promise 用try catch防止報錯
            try {
                let then = x.then;
                if (typeof then === 'function') {
                    //成功
                    then.call(x, function (y) {
                        if (called) return        //避免別人寫的promise中既走resolve又走reject的情況
                        called = true;
                        resolutionProcedure(promise2, y, resolve, reject)
                    }, function (err) {
                        if (called) return
                        called = true;
                        reject(err);
                    })
                } else {
                    resolve(x)             //如果then不是函數 則把x作爲返回值.
                }
            } catch (e) {
                if (called) return
                called = true;
                reject(e)
            }

        } else {  //普通值
            return resolve(x)
        }

    }

    Promise.prototype.then = function (onFulfilled, onRejected) {
        //成功和失敗默認不傳給一個函數
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) {
            return value;
        }
        onRejected = typeof onRejected === 'function' ? onRejected : function (err) {
            throw err;
        }
        let that = this;
        let promise2;  //新增: 返回的promise
        if (that.status === 'RESOLVED') {
            promise2 = new Promise(function (resolve, reject) {
                setTimeout(function () {                          //用setTimeOut實現異步
                    try {
                        let x = onFulfilled(that.value);        //x可能是普通值 也可能是一個promise, 還可能是別人的promise                               
                        resolutionProcedure(promise2, x, resolve, reject)  //寫一個方法統一處理 
                    } catch (e) {
                        reject(e);
                    }

                })
            })
        }
        if (that.status === 'REJECTED') {
            promise2 = new Promise(function (resolve, reject) {
                setTimeout(function () {
                    try {
                        let x = onRejected(that.value);
                        resolutionProcedure(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e);
                    }
                })
            })
        }

        if (that.status === 'PENDING') {
            promise2 = new Promise(function (resolve, reject) {
                that.resolvedCallbacks.push(function () {
                    setTimeout(function () {
                        try {
                            let x = onFulfilled(that.value);
                            resolutionProcedure(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e);
                        }
                    })
                });
                that.rejectedCallbacks.push(function () {
                    setTimeout(function () {
                        try {
                            let x = onRejected(that.value);
                            resolutionProcedure(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e);
                        }
                    })
                });
            })
        }
        return promise2;
    }
    Promise.defer = Promise.deferred = function () {
        let dfd = {};
        dfd.promise = new Promise((resolve, reject) => {
            dfd.resolve = resolve;
            dfd.reject = reject;
        });
        return dfd;
    }
    module.exports = Promise;

總結

以上就是符合Promise/A+規範的源碼,ES6的Promise其實並不是向我們這樣通過js來實現,而是在底層實現,並且還擴展了很多新的方法:

  • Promise.prototype.catch()
  • Promise.prototype.finally()
  • Promise.all()
  • Promise.race()
  • Promise.allSettled()
  • Promise.any()
  • Promise.resolve()
  • Promise.reject()
  • Promise.try()

這裏就不一一介紹啦,大家可以參考阮一峯老師的文章 ES6入門-Promise對象
這篇文章給大家講解的Promise/A+規範的源碼,希望大家能多讀多寫,深刻的體會一下源碼的思想,對以後的開發也很有幫助。
感謝大家的閱讀,覺得還不錯,辛苦點一下關注,謝謝!
pk.jpg

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章