再談Promise以及其實現-沒有基於Promise/A規範

大雄沒了哆啦A夢發表於2019-05-03

Promise的前世今生

在js中,非同步是一個非常重要的組成部分,它基於事件迴圈,保證了優先順序更高任務的優先執行權,比如js下載、UI渲染、js中非非同步的任務,非同步使得單程式的js能夠做到非阻塞,這在node顯得攸關重要,它使得js不必等待I/O操作返回結果,而能去處理其他任務。但是非同步也存在著缺點,最明顯的就是回地獄,而Pormise規範的出現,讓開發者從回撥地獄從解放出來。它由社群提出和實現,之後被es6加入到標準當中。

Promise的組成

Promise最基本的是由status、resolve、onResolved、reject、onRejected、then和catch幾部分組成:

status:狀態管理,有pendding,fulfilled和rejected三種狀態,且狀態一經改變是無法逆轉的;
resolve: 一個函式,當呼叫該函式時,說明非同步任務執行成功了,promise的status由pendding轉為fulfilled,且會呼叫成功的回撥函式onResolved;
reject: 一個函式,當呼叫該函式時,說明非同步任務執行失敗,promise的status由pendding轉為rejected,且會呼叫失敗的回撥函式onRejected;
then: 一個函式,在then的引數裡面,我們需要定義回撥成功需要執行的成功回撥函式,promise會將這個成功回撥函式註冊到onResolved,由resolve觸發呼叫,並且還支援鏈式呼叫;
catch: 一個函式,在reject的引數裡面,我們需要回撥失敗需要執行的失敗回撥函式,promise會將這個失敗回撥函式註冊到onRejected,由reject觸發呼叫;

image

備註: 這裡沒講race和all方法等,有興趣的可以去看下文件。

使用方法

通過new建立一個Promise物件,傳入一個函式引數,這個函式包含resolve和reject引數,這兩個引數,如上所述也是函式,當非同步任務完成的時候,呼叫resolve方法,當非同步任務失敗的時候,呼叫reject方法。eg:

var p = new Promise(function(resolve, reject){
    setTimeout(function() {
        resolve(1)
    }, 1000)
})
複製程式碼

then方法,引數也是一個回撥函式,當你呼叫resolve方法後,這個回撥函式就會呼叫,且resolve函式傳入的引數,會帶給這個回撥函式的引數,eg:

p.then(function(arg){
    console.log(arg) // 1
})
複製程式碼

catch方法,引數同樣是一個回撥函式,當你呼叫reject方法後,這個回撥函式就會呼叫,且reject函式傳入的引數,會帶給這個回撥函式,使用例子和then同理。
所以我們的Promise的呼叫可以是這樣的:

var p = new Promise(function(resolve, reject){
    // 非同步任務執行,且看結果呼叫resolve還是reject
}).then(function(){
    // do something1
    return new Promise...
}).then(function() {
    // do something2
    return new Promise....
}).then(function() {
    // do something3
    return new Promise....
}).then(function() {
    // do something4
    return new Promise....
})
複製程式碼

對比用回撥的方式

doAsyncTask1(function(){
    // do something1
    doAsyncTask2(function() {
        // do something2
        doAsyncTask3(function() {
            // do something3
            doAsyncTask4(function(){
                // do something4
            })
        })
    })
    
})
複製程式碼

是不是比起非同步程式設計,promise更人性化呢?

async await

上面promise的呼叫方式其實還不夠優雅,還有更加優雅的呼叫方式,那就是async await方式,我們來看下如何用。

async testPromise() {
   var result = await new Promise(function(resolve, reject){
       setTimeout(function(){
           resolve(1)
       }, 2000)
   })
   console.log(result)
   var result1 = await new Promise(function(resolve, reject){
       setTimeout(function(){
           resolve(1)
       }, 2000)
   })
   console.log(result1)
}
複製程式碼

是不是已經變成了我們同步的程式設計方式了?
這裡有兩點需要注意的:

  1. await關鍵字必須在使用async修飾的方法中才能使用,反之則沒問題。
  2. async不能直接使用,需要使用babel進行轉譯,一般需要藉助webpack進行配置,不能單獨在js中使用,這個相對來說比較麻煩。

如何實現自己實現Promise

面試官很喜歡問你如何實現一個Promise,這個時候我們就需要對Promise有一定的理解,我們不去看Promise/A+規範,太複雜晦澀難懂了,我們就按照上面講的Promise的組成來實現一個簡單的Promise。

function _Promise(fn) {
    this.status = 'pending';
    this.onResolved = null;
    this.onRejected = null;
    
    fn.call(this, this.resolve.bind(this), this.reject.bind(this))
    
}

_Promise.prototype.resolve = function(arg) {
    if(this.status === 'pending') {
        this.onResolved(arg)
        this.status = 'fulfilled'
    }
}
_Promise.prototype.reject = function(arg) {
    if(this.status === 'pending') {
        this.onRejected(arg)
        this.status = 'rejected'
    }
}

_Promise.prototype.then = function(onResolved) {
    this.onResolved = onResolved
}

_Promise.prototype.reject = function(onRejected) {
    this.onResolved = onRejected
}
複製程式碼

這就是一個最簡單的Promise實現方式,但是它還不支援鏈式呼叫。所以我們需要在then方法再返回一個Promise, 我們改造一下:

function _Promise(fn) {
    this.status = 'pending';
    this.onResolved = null;
    this.onRejected = null;
    this.childReject = null;
    this.childResolve = null;
    
    fn.call(this, this.resolve.bind(this), this.reject.bind(this))
    
}

_Promise.prototype.resolve = function(arg) {
    if(this.status === 'pending') {
        if(this.onResolved) {
            var ret = this.onResolved(arg)
            // 如果第二個then return的是一個使用者自定義的promise,我們稱為PromiseB,則需要把childResolve賦值給這個PromiseB的onResolved,交給這個PromiseB來執行
            if(ret instanceof _Promise) {
                ret.onResolved = this.childResolve
                return
            }
            // 否則直接呼叫childResove確保第二個then的回撥執行
            this.childResolve && this.childResolve()
        }
        this.status = 'fulfilled'
    }
    
}
_Promise.prototype.reject = function(arg) {
    if(this.status === 'pending') {
        this.onRejected && this.onRejected(arg)
        this.childReject && this.childReject()
        this.status = 'rejected'
    }
}

_Promise.prototype.then = function(onResolved) {
    this.onResolved = onResolved
    var that = this
    // 定義一個promise,我們簡稱PromiseA,以便鏈式呼叫
    return new _Promise(function(resolve, reject){
      // 這裡需要確保resolve方法在第一個promise的resolve呼叫後呼叫,那麼需要把它儲存到第一個promise裡面
      that.childResolve = resolve
      that.childReject = reject
    })
}

_Promise.prototype.reject = function(onRejected) {
    this.onResolved = onRejected
}

// 例子
new _Promise((resolve) => {
    setTimeout(() => {
        resolve()
    }, 2000)
}).then(() => {
    console.log('promise success')
    return new _promise((resolve) => {
        setTimeout(() => {
            resolve()
        }, 1000)
    })
}).then(() => {
    console.log('promise2 success')
})

複製程式碼

這個實現,有三個Promise,第一個是首個Promise,第二個PormiseA即使如果then方法沒有返回使用者自定義的Promise的時候,我們方便鏈式呼叫的Promise,第三個PromiseB是then方法返回使用者自定義的Promise的時候,我們需要把第二個then的回撥交還給PromiseB執行。

相關文章