Promise原理探究及實現

前言

做爲ES6處理異步操做的新規範,Promise一經出現就廣受歡迎。面試中也是如此,固然此時對前端的要求就不只僅侷限會用這個階段了。下面就一塊兒看下Promise相關的內容。javascript

Promise用法及實現

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

定義

Promise 是異步編程的一種解決方案,能夠認爲是一個對象,能夠從中獲取異步操做的信息。以替代傳統的回調事件。java

常見用法

Promise的建立

es6規範中,Promise是個構造函數,因此建立以下:git

const promise = new Promise((resolve, reject) => {
    setTimeout(resolve, 200, 'resolve');
    // 能夠爲同步,以下操做
    return resolve('resolve')
})
複製代碼

注意resolve或者reject 一旦執行,後續的代碼能夠執行但就不會再更新狀態(不然這狀態回調就沒法控制了)。 舉個例子:es6

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。 邏輯代碼推薦在狀態更新以前執行。github

構造函數

構造函數接收一個函數,該函數會同步執行,即咱們的邏輯處理函數,什麼時候執行對應的回調,這部分邏輯仍是要本身管理的。面試

至於如何執行回調,就和入參有關係了。
兩個入參resolve和reject,分別更新不一樣狀態,以觸發對應處理函數。 觸發操做由Promise內部實現,咱們只關注觸發時機便可編程

構造函數實現

那麼要實現一個Promise,其構造函數應該是這麼個樣子:json

// 三種狀態 
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 作的事情主要兩個:promise

  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入門
promisesaplus.com/
liubin.org/promises-bo…

本想把常見的promise面試題一塊兒加上的,後面就寫成了promise的實現,手動Promise均可以實現的話,相關面試題應該問題不大。這裏附一個JavaScript | Promises interiew 你們能夠看看。完整代碼請戳

相關文章
相關標籤/搜索