前端基礎(十):promise

大司馬愛學習發表於2018-08-17

字數:2874

閱讀時間:10分鐘

前言

promise是非同步呼叫的解決方案,它有業界公認的標準:www.ituring.com.cn/article/665… 有許多優秀的實現外掛,如:Jquery、Q等,ES6中也新增了該特性。

它的使用方式也很簡單,如下程式碼:

let promise = new Promise(function(resolve, reject){
    setTimeout(function(){
        resolve('success');
    },1000);
});

promise.then(function(response){
   console.log(response); 
});

//也可以使用defer語法糖,省略建立Promise的過程
let defer = Promise.defer();
setTimeout(function(){
    defer.resolve('success');
},1000);

defer.promise.then(function(response){
   console.log(response); 
})
複製程式碼

但是如果我們想知道更多的內容,如:

1.promise中的執行器引數什麼時候執行的。

2.then函式中的回撥函式什麼時候執行。

3.鏈式呼叫then函式時,它的執行順序是什麼樣的;如果有一個promise被拒絕了,後續如何呼叫;如果程式碼出現異常,後續then函式如何呼叫;如果上一個回撥函式返回了一個promise物件,又是如何執行的...

這時,最好的辦法莫過於檢視實現原始碼了,但更好的辦法是我們自己去實現一次。因此,本文我會與大夥一起基於A+規範實現一個自定義的Promise,旨在更好地理解promise的用法。

正文

先上標準,

中文:www.ituring.com.cn/article/665…

英文:promisesaplus.com/

一、實現一個promise類

基於上述標準,理清思路:

1.我們需要實現一個Promise類,其建構函式接受一個執行器引數,並且會在建構函式中呼叫該執行器。

2.Promise應有當前狀態、終值、原因三個屬性。

3.Promise應該實現執行、拒絕兩個動作。

4.由於then是可以多次呼叫的,所以,Promise中應該有儲存執行動作和拒絕動作的兩個佇列。

5.Promise應該暴露一個then函式,用來處理回撥函式。

按照上述思路,我們編寫出如下程式碼:

/**
 * 自定義promise物件
 */
class Promise {
    /**
     * 建構函式
     * @param  {Function} excutor 執行器
     */
    constructor (excutor) {
        //promise狀態,有pending、resolved、rejected
        this.status = 'pending';

        //終值
        this.value;

        //拒因
        this.reason;

        //解決函式佇列
        this.resolveFuns = [];

        //拒絕函式佇列
        this.rejectFuns = [];

        //解決函式
        let resolve = val => {
            if (this.status === 'pending') {
                this.status = 'resolved';
                this.value = val;
                this.resolveFuns.forEach(func => func());
            }
        };

        //拒絕函式
        let reject = reason => {
            if (this.status === 'pending') {
                this.status = 'rejected';
                this.reason = reason;
                this.rejectFuns.forEach(func => func());
            }
        };

        try {
            excutor(resolve, reject);
        } catch (ex) {
            reject(ex);
        }
    }

    /**
     * 回撥函式處理
     * @param  {Function} resolveCallBack 執行回撥函式
     * @param  {Function} rejectCallBack  拒絕回撥函式
     */
    then (resolveCallBack, rejectCallBack) {
        
    }
}
複製程式碼

參照規範:

一個 Promise 的當前狀態必須為以下三種狀態中的一種:等待態(Pending)執行態(Fulfilled)拒絕態(Rejected)

這裡我們status屬性對應如上三種狀態(我們更習慣使用resolved來表示執行,因此沒有使用fulfilled)。

建構函式中,resolve和reject函式的實現中,遵循了規範中對狀態變化的規定:

等待態(Pending)

處於等待態時,promise 需滿足以下條件:

  • 可以遷移至執行態或拒絕態

執行態(Fulfilled)

處於執行態時,promise 需滿足以下條件:

  • 不能遷移至其他任何狀態
  • 必須擁有一個不可變的終值

拒絕態(Rejected)

處於拒絕態時,promise 需滿足以下條件:

  • 不能遷移至其他任何狀態
  • 必須擁有一個不可變的據因

如果執行器函式執行報錯,則直接執行拒絕動作。因此,如果我們使用promise時,傳入的執行器執行報錯,則會在then函式中的拒絕回撥函式中捕獲到錯誤。但是,如果是執行器函式中的延時操作報錯(例setTimeout中報錯),這裡是無法捕獲到錯誤的。

二、實現then函式

基於標準,理清思路:

1.then函式接受兩個引數:執行回撥函式和拒絕回撥函式。如果傳入的引數並非函式,則忽略它並將資訊傳遞到下一個then函式中。

2.then函式必須返回一個新的promise。

3.如果promise狀態為pending,則應該將回撥函式推入佇列;如果狀態為resolved,則應直接呼叫執行回撥函式;如果狀態為reject,則應直接呼叫拒絕回撥函式。如果回撥函式執行報錯,則應該執行下一個then函式中的決絕回撥函式。

4.需要處理then的鏈式呼叫。

5.所有的回撥函式都應該非同步執行(為了保證then函式執行順序的一致性。)

首先,我們編寫一個對應引數和返回值的函式,程式碼如下:

	/**
     * 回撥函式處理
     * @param  {Function} resolveCallBack 執行回撥函式
     * @param  {Function} rejectCallBack  拒絕回撥函式
     * @return {promise}                 新建立的promise
     */
    then (resolveCallBack, rejectCallBack) {
        let pDeffer = Promise.defer();
        if (this.status === 'pending') {
            //如果promise狀態為pending則將回撥函式加入佇列中
            
        } else if (this.status === 'resolved') {
            //如果promise狀態為resolved,則立即執行resolveCallBack回撥函式,
            //如此則無論promise是否已經執行完畢,回撥函式都必然會執行
            
        } else if (this.status === 'rejected') {
            //如果promise狀態為rejected,則立即執行rejectedCallBack回撥函式,
            //原因同上
           	
        }

        return pDeffer.promise;
    }
複製程式碼

其中defer函式是我在Promise物件上建立的一個靜態函式,是一個建立Promise物件的語法糖函式,其程式碼如下:

/**
 * 建立一個預設的promise(語法糖)
 * @return {Object} deffer物件
 */
Promise.defer = Promise.deferred = () => {
    let defer = {};
    defer.promise = new Promise((resolve, reject) => {
        defer.resolve = resolve;
        defer.reject = reject;
    });
    return defer;
};
複製程式碼

然後根據整理的思路1中,需要處理引數不為函式的情況。執行回撥函式直接返回接受的返回值即可,拒絕回撥函式丟擲對應原因的異常即可(這裡只有丟擲異常,下一個then函式才會呼叫拒絕回撥函式)。得到如下程式碼:

	then (resolveCallBack, rejectCallBack) {
        //如果resolveCallBack不是函式,則將值傳遞到下一個resolveCallBack函式中
        resolveCallBack = Promise.isFunction(resolveCallBack) ? resolveCallBack : x => x;

        //如果rejectCallBack不是函式,則將原因傳遞到下一個rejectCallBack函式中
        rejectCallBack = Promise.isFunction(rejectCallBack) ? rejectCallBack : reason => {
            throw reason;
        };
        ...
    }
複製程式碼

然後,我們需要處理回撥函式邏輯。三種狀態的處理邏輯大致一致,我們就以最為複雜的pendding狀態為例。

當狀態為pendding時,我們需要將執行函式和拒絕函式分別推入promise的執行函式佇列和拒絕函式佇列中。該函式必須非同步執行,並且如果執行報錯,則直接執行下一個then的拒絕回撥函式。還有,我們需要考慮終值可能會是一個promise物件的情況,這種情況我們封裝一個函式在第三步進行實現。得到如下程式碼:

	/**
     * 回撥函式處理
     * @param  {Function} resolveCallBack 執行回撥函式
     * @param  {Function} rejectCallBack  拒絕回撥函式
     * @return {promise}                 新建立的promise
     */
    then (resolveCallBack, rejectCallBack) {
        //如果resolveCallBack不是函式,則將值傳遞到下一個resolveCallBack函式中
        resolveCallBack = Promise.isFunction(resolveCallBack) ? resolveCallBack : x => x;

        //如果rejectCallBack不是函式,則將原因傳遞到下一個rejectCallBack函式中
        rejectCallBack = Promise.isFunction(rejectCallBack) ? rejectCallBack : reason => {
            throw reason;
        };

        /**
         * 解決promise函式
         * @param  {promise} promise 待解決的promise
         * @param  {*} x       上一個promise的終值
         * @param  {Function} resolve promise的執行回撥函式
         * @param  {Function} reject  promise的拒絕回撥函式
         */
        function resolvePromise (promise, x, resolve, reject) {
            
        }

        let pDeffer = Promise.defer();
        if (this.status === 'pending') {
            //如果promise狀態為pending則將回撥函式加入佇列中
            //新增解決函式佇列
            this.resolveFuns.push(() => {
                setTimeout(() => {
                	try {
	                    let x = resolveCallBack(this.value);
	                    resolvePromise(pDeffer.promise, x, pDeffer.resolve, pDeffer.reject);
                	} catch (ex) {
                		pDeffer.reject(ex);
                	}
                }, 0);
            });

            //新增拒絕函式佇列
            this.rejectFuns.push(() => {
                setTimeout(() => {
                	try {
	                    let x = rejectCallBack(this.reason);
	                    resolvePromise(pDeffer.promise, x, pDeffer.resolve, pDeffer.reject);
                	} catch (ex) {
                		pDeffer.reject(ex);
                	}
                }, 0);
            });
        } else if (this.status === 'resolved') {
            //如果promise狀態為resolved,則立即執行resolveCallBack回撥函式,
            //如此則無論promise是否已經執行完畢,回撥函式都必然會執行
            setTimeout(() => {
            	try {
	                let x = resolveCallBack(this.value);
	                resolvePromise(pDeffer.promise, x, pDeffer.resolve, pDeffer.reject);
            	} catch (ex) {
            		pDeffer.reject(ex);
            	}
            }, 0);
        } else if (this.status === 'rejected') {
            //如果promise狀態為rejected,則立即執行rejectedCallBack回撥函式,
            //原因同上
            setTimeout(() => {
            	try {
	                let x = rejectCallBack(this.reason);
	                resolvePromise(pDeffer.promise, x, pDeffer.resolve, pDeffer.reject);
            	} catch (ex) {
            		pDeffer.reject(ex);
            	}
            }, 0);
        }

        return pDeffer.promise;
    }
複製程式碼

三、實現解決promise函式

這一步就是為了實現上述的resolvePromise函式。

整理思路如下:

1.如果終值x與promise是同一個引用,則丟擲異常(死迴圈)。

2.如果x是具有then函式的函式或物件,則遞迴呼叫。由於x是外部不可控變數,所以我們要保證只執行一次執行回撥函式或者拒絕回撥函式。

3.如果x並非具有then函式的函式或物件,則直接呼叫執行回撥函式。

4.如果其中執行報錯,則呼叫拒絕回撥函式。

根據思路,編寫出如下程式碼:

/**
* 解決promise函式
* @param  {promise} promise 待解決的promise
* @param  {*} x       上一個promise的終值
* @param  {Function} resolve promise的執行回撥函式
* @param  {Function} reject  promise的拒絕回撥函式
*/
function resolvePromise (promise, x, resolve, reject) {
    if (x === promise) {
        reject(new TypeError('終值與Promise相等,陷入死迴圈!'));
        return;
    }
    let bCalled = false;
    if (x != null && (Promise.isFunction(x) || Promise.isObject(x))) {
        try {
            let then = x.then;
            if (Promise.isFunction(then)) {
                then.call(x, y => {
                    if (bCalled === true) {
                        return;
                    }
                    bCalled = true;
                    resolvePromise(promise, y, resolve, reject);
                }, r => {
                    if (bCalled === true) {
                        return;
                    }
                    bCalled = true;
                    reject(r);
                });
            } else {
                if (bCalled === true) {
                    return;
                }
                bCalled = true;
                resolve(x);
            }
        } catch (ex) {
            if (bCalled === true) {
                return;
            }
            bCalled = true;
            reject(ex);
        }
    } else {
        if (bCalled === true) {
            return;
        }
        bCalled = true;
        resolve(x);
    }
}
複製程式碼

這裡有兩個注意點:

1.宣告一個變數接收x.then變數,是為了避免多次訪問x.then屬性導致其值在檢索時發生改變。這個可能有點難以理解,我們看一下 promises-aplus-tests 中的測試用例原始碼就更容易理解了:

describe("`x` is an object with normal Object.prototype", function () {
    var numberOfTimesThenWasRetrieved = null;

    beforeEach(function () {
        numberOfTimesThenWasRetrieved = 0;
    });

    function xFactory() {
        return Object.create(Object.prototype, {
            then: {
                get: function () {
                    ++numberOfTimesThenWasRetrieved;
                    return function thenMethodForX(onFulfilled) {
                        onFulfilled();
                    };
                }
            }
        });
    }

    testPromiseResolution(xFactory, function (promise, done) {
        promise.then(function () {
            assert.strictEqual(numberOfTimesThenWasRetrieved, 1);
            done();
        });
    });
});
複製程式碼

如上述程式碼,每次我們呼叫then屬性時,都會導致numberOfTimesThenWasRetrieved的值加1,從而發生一些不可預期的問題。

2.當x為promise物件時,手動呼叫x的then函式,然後將返回值作為終值傳入promise中,就是為了處理程式碼執行順序的問題。只有當上一then函式中的程式碼執行完畢,才會執行下一個then函式中的回撥函式。因此,我們在使用鏈式呼叫時,上一then函式中執行回撥函式中往往會返回一個promise物件,就是這個原理。

編碼工作到此完畢。我們可以使用promises-aplus-tests外掛跑一把看看結果:

前端基礎(十):promise

完整程式碼地址:

github.com/iTrustGuid/…

歡迎關注我的微信公眾號:

前端基礎(十):promise

相關文章