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

Promise/A+ 規範,源碼分析

GitHub
前端

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

ES6入門-Promise對象git

若是你已經掌握了Promise的基本用法,咱們進行下一步es6

Promise/A+規範

說到Promise/A+規範,不少同窗可能很不理解這是一個什麼東西,下面給出兩個地址,不瞭解的同窗須要先了解一下,對咱們後續理解源碼頗有幫助,先看兩遍,有些地方看不懂也不要緊,後續咱們能夠經過源碼來回頭再理解,想把一個知識真的學會,就要反覆琢磨,從【確定->否認->再確定】不斷地深刻理解,直到徹底掌握。github

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

若是你看過了Promise/A+規範,咱們繼續,我會帶着你們按照規範要求,一步一步的來實現源碼算法

Promise/A+ 【2.1】

2.1Promise狀態

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

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 = 'pending';
    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構造函數的初步搭建,包含:promise

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

下面咱們來完成resolve和rejectbash

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)

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

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);
                        }
                    })
                });
            })
        }
        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);
                    }
                })
            })
        }
複製代碼
  • 爲了保證函數執行順序,須要將函數體代碼使用 setTimeout 包裹起來
  • 首先咱們返回了一個新的 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/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 (value instanceof Promise) {
                return value.then(resolve, reject)
            }
            if (that.status === 'PENDING') {
                that.status = 'RESOLVED';
                that.value = value;
                that.resolvedCallbacks.map(cb => cb(that.value));
            }
        }
        function reject(value) {
            if (that.status === 'PENDING') {
                that.status = 'REJECTED';
                that.value = value;
                that.rejectedCallbacks.map(cb => cb(that.value));
            }
        }
        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 === '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);
                        }
                    })
                });
            })
        }
        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);
                    }
                })
            })
        }

        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+規範的源碼,但願你們能多讀多寫,深入的體會一下源碼的思想,對之後的開發也頗有幫助。
感謝你們的閱讀,以爲還不錯,辛苦點一下關注,謝謝!

相關文章
相關標籤/搜索