Promise原理探究及實現

瀟湘待雨發表於2019-07-18

前言

作為ES6處理非同步操作的新規範,Promise一經出現就廣受歡迎。面試中也是如此,當然此時對前端的要求就不僅僅侷限會用這個階段了。下面就一起看下Promise相關的內容。

Promise用法及實現

在開始之前,還是簡單回顧下Promise是什麼以及怎麼用,直接上來談實現有點空中花園的感覺。(下面示例參考自阮大佬es6 Promis,)

定義

Promise 是非同步程式設計的一種解決方案,可以認為是一個物件,可以從中獲取非同步操作的資訊。以替代傳統的回撥事件。

常見用法

Promise的建立

es6規範中,Promise是個建構函式,所以建立如下:

const promise = new Promise((resolve, reject) => {
    setTimeout(resolve, 200, 'resolve');
    // 可以為同步,如下操作
    return resolve('resolve')
})

注意resolve或者reject 一旦執行,後續的程式碼可以執行但就不會再更新狀態(否則這狀態回撥就無法控制了)。
舉個例子:

var a = new Promise((resolve,reject)=>{
    resolve(1)
    console.log('執行程式碼,改變狀態')
    throw new Error('ss')
})
a.then((res)=>{
    console.log('resolved >>>',res)
},(err)=>{
    console.log('rejected>>>',err)  
})

// 輸出
// 執行程式碼,改變狀態
// resolved >>> 1

因此,狀態更新函式之後的再次改變狀態的操作都是無效的,例如異常之類的也不會被catch。
邏輯程式碼推薦在狀態更新之前執行。

建構函式

建構函式接收一個函式,該函式會同步執行,即我們的邏輯處理函式,何時執行對應的回撥,這部分邏輯還是要自己管理的。

至於如何執行回撥,就和入參有關係了。
兩個入參resolve和reject,分別更新不同狀態,以觸發對應處理函式。
觸發操作由Promise內部實現,我們只關注觸發時機即可

建構函式實現

那麼要實現一個Promise,其建構函式應該是這麼個樣子:

// 三種狀態 
const STATUS = {
    PENDING: 'pending',
    RESOLVED:'resolved',
    REJECTED:'rejected'
}    
class Promise{
    constructor(fn){
        // 初始化狀態
        this.status = STATUS.PENDING
        // resolve事件佇列
        this.resolves = []
        // reject事件佇列
        this.rejects = [] 
        // resolve和reject是內部提供的,用以改變狀態。
        const resovle = (val)=>{
           // 顯然這裡應該是改變狀態觸發回撥
           this.triggerResolve(val)
        }
        const reject = (val)=>{
           // 顯然這裡應該是改變狀態觸發回撥
           this.triggerReject(val)
        }
        // 執行fn
        try{
            fn(resolve,reject)
       }catch(err){
           // 執行異常要觸發reject,就需要在這裡catch了
           this.triggerReject(err)
       }
    }
    then(){
    }
}

觸發回撥的triggerReject/triggerResolve 做的事情主要兩個:

  1. 更新當前狀態
  2. 執行回撥佇列中的事件
    // 觸發 reject回撥  
    triggerReject(val){
        // 儲存當前值,以供後面呼叫
        this.value = val
        // promise狀態一經變化就不再更新,所以對於非pending狀態,不再操作
        if (this.status === STATUS.PENDING) {
            // 更新狀態
            this.status = STATUS.REJECTED
            // 迴圈執行回撥佇列中事件
            this.rejects.forEach((it) => {
                it(val)
            })
        }
    }
    // resolve 功能類似
    // 觸發 resolve回撥
    triggerResolve(val) {
        this.value = val
        if(this.status === STATUS.PENDING){
            this.status = STATUS.RESOLVED
            this.resolves.forEach((it,i)=>{
                it(val)
            })
        }
    }

此時執行的話還是不能達到目的的,因為this.resolves/ this.rejects的回撥佇列裡面還是空呢。
下面就看如何會用then往回撥佇列中增加監聽事件。

then用法

該方法為Promise例項上的方法,作用是為Promise例項增加狀態改變時的回撥函式。
接受兩個引數,resolve和reject即我們所謂成功和失敗回撥,其中reject可選

then方法返回的是一個新的例項(也就是新建了一個Promise例項),可實現鏈式呼叫。

new Promise((resolve, reject) => {
  return resolve(1)
}).then(function(res) {
  // ...
}).then(function(res) {
  // ...
});

前面的結果為後邊then的引數,這樣可以實現次序呼叫。
若前面返回一個promise,則後面的then會依舊遵循promise的狀態變化機制進行呼叫。

then 實現

看起來也簡單,then是往事件佇列中push事件。那麼很容易得出下面的程式碼:

// 兩個入參函式
then(onResolved,onRejected){
    const resolvehandle=(val)=>{
          return   onResolved(val)
    },rejecthandle =(val)=>{
          return   onRejected(val)
    }
    // rejecthandle 
    this.resolves.push(resolvehandle)
    this.rejects.push(rejecthandle)
}

此時執行示例程式碼,可以得到結果了。

new Promise((resolve, reject) => {
    setTimeout(resolve, 200, 'done');
}).then((res)=>{
    console.log(res)
}) // done

不過這裡太簡陋了,而且then還有個特點是支援鏈式呼叫其實返回的也是promise 物件。
我們來改進一下。

then支援鏈式呼叫

 then(onResolved,onRejected){
        // 返回promise 保證鏈式呼叫,注意這裡每次then都新建了promise
        return new Promise((resolve,reject)=>{
            const resolvehandle = (val)=>{
                // 對於值,回撥方法存在就直接執行,否則不變傳遞下去。
                let res = onResolved ? onResolved(val) : val
                if(Promise.isPromise(res)){
                    // 如果onResolved 是promise,那麼就增加then
                    return res.then((val)=>{
                        resolve(val)
                    })
                }else {
                    // 更新狀態,執行完了,後面的隨便
                    return resolve(val)
                }
            },
            rejecthandle = (val)=>{
                var res = onRejected ? onRejected(val) : val;
                if (Promise.isPromise(res)) {
                    res.then(function (val) {
                        reject(val);
                    })
                } else {
                    reject(val);
                }
            }
            // 正常加入佇列
            this.resolves.push(resolvehandle)
            this.rejects.push(rejecthandle)
        })
    }        

此時鏈式呼叫和promise 的回撥也已經支援了,可以用如下程式碼測試。

new Promise((resolve, reject) => {
    setTimeout(resolve, 200, 'done');
}).then((res)=>{
    return new Promise((resolve)=>{
        console.log(res)
        setTimeout(resolve, 200, 'done2');
    })
}).then((res)=>{
    console.log('second then>>', res)
})

同步resolve的實現

不過此時對於同步的執行,還是有些問題。
因為then中的實現,只是將回撥事件假如回撥佇列。
對於同步的狀態,then執行在建構函式之後,
此時事件佇列為空,而狀態已經為resolved,
所以這種狀態下需要加個判斷,如果非pending狀態直接執行回撥。

 then(onResolved,onRejected){
             /**省略**/
            // 剛執行then 狀態就更新,那麼直接執行回撥
            if(this.status === STATUS.RESOLVED){
                return resolvehandle(this.value)
            }
            if (this.status === STATUS.REJECTED){
                return rejecthandle(this.value)
            }    
        })
    }        

這樣就能解決同步執行的問題。

new Promise((resolve, reject) => {
    resolve('done')
}).then((res)=>{
    console.log(res)
})
// done

catch

catch方法是.then(null, rejection)或.then(undefined, rejection)的別名,用於指定發生錯誤時的回撥函式。
直接看例子比較簡單:

getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // 處理 getJSON 和 前一個回撥函式執行時發生的錯誤
  console.log('發生錯誤!', error);
});

此時catch是是getJSON和第一個then執行時的異常,如果只是在then中指定reject函式,那麼then中執行的異常無法捕獲。
因為then返回了一個新的promise,同級的reject回撥,不會被觸發。
舉個例子:

var a = new Promise((resolve,reject)=>{
    resolve(1)
})
a.then((res)=>{
    console.log(res)
    throw new Error('then')
},(err)=>{
    console.log('catch err>>>',err)  // 不能catch
})

該catch只能捕獲建構函式中的異常,對於then中的error就不能捕獲了。

var a = new Promise((resolve,reject)=>{
   resolve(1)
})
a.then((res)=>{
    console.log(res)
    throw new Error('then')
}).catch((err)=>{
    console.log('catch err>>>',err) // catch err>>> Error: then  at <anonymous>:6:11
})

推薦每個then之後都跟catch來捕獲所有異常。

catch 的實現

基於catch方法是.then(null, rejection)或.then(undefined, rejection)的別名這句話,其實實現就比較簡單了。
其內部實現呼叫then就可以了。

catch(onRejected){
        return this.then(null, onRejected)
    }

Promise.resolve/Promise.reject

該方法為獲取一個指定狀態的Promise物件的快捷操作。
直接看例子比較清晰:

Promise.resolve(1);
// 等價於
new Promise((resolve) => resolve(1));
Promise.reject(1);
// 等價於
new Promise((resolve,reject) => reject(1));

既然是Promise的自身屬性,那麼可以用es6的static來實現:
Promise.reject與其類似,就不再實現了。

    // 轉為promise resolve 狀態
    static resolve(obj){
        if (Promise.isPromise(obj)) {
            return obj;
        }
        // 非promise 轉為promise
        return new Promise(function (resolve, reject) {
            resolve(obj);
        })
    }

結束語

參考文章

阮一峰es6入門
https://promisesaplus.com/
http://liubin.org/promises-book/#chapter1-what-is-promise

本想把常見的promise面試題一起加上的,後面就寫成了promise的實現,手動Promise都可以實現的話,相關面試題應該問題不大。這裡附一個JavaScript | Promises interiew 大家可以看看。完整程式碼請戳

相關文章