Build Your Own Promise

1、JavaScript異步編程背景

​ 從去年ES2015發佈至今,已通過去了一年多,ES2015發佈的新的語言特性中最爲流行的也就莫過於Promise了,Promise使得現在JavaScript異步編程如此輕鬆愜意,甚至慢慢遺忘了曾經那不堪回首的痛楚。其實從JavaScript誕生,JavaScript中的異步編程就已經出現,例如點擊鼠標、敲擊鍵盤這些事件的處理函數都是異步的,時間到了2009年,Node.js橫空出世,在整個Node.js的實現中,將回調模式的異步編程機制發揮的淋漓盡致,Node的流行也是的愈來愈多的JavaScripter開始了異步編程,可是回調模式的反作用也慢慢展示在人們眼前,錯誤處理不夠優雅以及嵌套回調帶來的「回調地獄」。這些反作用使得人們從回調模式的溫柔鄉中慢慢清醒過來,開始尋找更爲優雅的異步編程模式,路漫漫其修遠兮、吾將上下而求索。時間到了2015年,Promise拯救那些苦苦探索的先驅。行使它歷史使命的時代彷佛已經到來。javascript

​ 每一個事物的誕生有他的歷史使命,更有其歷史成因,促進其被那些探索的先驅們所發現。瞭解nodejs或者熟悉瀏覽器的人都知道,JavaScript引擎是基於事件循環或單線程這兩個特性的。更爲甚者在瀏覽器中,更新UI(也就是瀏覽器重繪、重拍頁面佈局)和執行JavaScript代碼也在一個單線程中,可想而知,一個線程就至關於只有一條馬路,若是一輛馬車拋錨在路上了阻塞了馬路,那麼別的馬車也就擁堵在了那兒,這個單線程容易被阻塞是一個道理,單線程也只能容許某一時間點只可以執行一段代碼。同時,JavaScript沒有想它的哥哥姐姐們那麼財大氣粗,像Java或者C++,一個線程不夠,那麼再加一個線程,這樣就可以同時執行多段代碼了,可是這樣就會帶來的隱患就是狀態不容易維護,JavaScript選擇了單線程非阻塞式的方式,也就是異步編程的方式,就像上面的馬車拋錨在了路上,那麼把馬車推到路邊的維修站,讓其餘馬車先過去,等馬車修好了再回到馬路上繼續行駛,這就是單線程非阻塞方式。正如Promise的工做方式同樣,經過Promise去向服務器發起一個請求,畢竟請求有網絡開銷,不可能立刻就返回請求結果的,這個時候Promise就處於pending狀態,可是其並不會阻塞其餘代碼的執行,當請求返回時,修改Promise狀態爲fulfilled或者rejected(失敗請求)。同時執行綁定到這兩個狀態上面的「處理函數」。這就是異步編程的模式,也就是Promise兢兢業業的工做方式,在下面一個部分將詳細討論Promise。java

2、Promise基礎

​ 怎麼一句話解釋Promise呢?Promise能夠代指那些還沒有完成的一些操做,可是其在將來的某個時間會返回某一特定的結果。 node

​ 當建立一個Promise實例後,其表明一個未知的值,在未來的某個時間會返回一個成功的返回值,或者失敗的返回值,咱們能夠爲這些返回值添加處理函數,當值返回時,處理函數被調用。Promise老是處於下面三種狀態之一:git

  • pending: Promise的初始狀態,也就是未被fulfilled或者rejected的狀態。es6

  • fulfilled: 意味着promise代指的操做已經成功完成。github

  • rejected:意味着promise代指的操做因爲某些緣由失敗。編程

一個處於pending狀態的promise可能因爲某個成功返回值而發展爲fulfilled狀態,也有可能由於某些錯誤而進入rejected狀態,不管是進入fulfilled狀態或者rejected狀態,綁定到這兩種狀態上面的處理函數就會被執行。而且進入fulfilled或者rejected狀態也就不能再返回pending狀態了。promise

3、邊學邊寫

上面說了那麼多,其實都是鋪墊。接下來咱們就開始實現本身的Promise對象。go go go!!!瀏覽器

第一步:Promise構造函數

Promise有三種狀態,pending、fulfilled、rejected。緩存

const PENDING = 'PENDING' // Promise 的 初始狀態
const FULFILLED = 'FULFILLED' // Promise 成功返回後的狀態
const REJECTED = 'REJECTED' // Promise 失敗後的狀態

有了三種狀態後,那麼咱們怎麼建立一個Promise實例呢?

const promise = new Promise(executor) // 建立Promise的語法

經過上面生成promise語法咱們知道,Promise實例是調用Promise構造函數經過new操做符生成的。這個構造函數咱們能夠先這樣寫:

class Promise {
    constructor(executor) {
        this.status = PENDING // 建立一個promise時,首先進行狀態初始化。pending
        this.result = undefined // result屬性用來緩存promise的返回結果,能夠是成功的返回結果,或失敗的返回結果
    }
}

咱們能夠看到上面構造函數接受的參數executor。它是一個函數,而且接受其餘兩個函數(resolve和reject)做爲參數,當resolve函數調用後,promise的狀態轉化爲fulfilled,而且執行成功返回的處理函數(不用着急後面會說到怎麼添加處理函數)。當reject函數調用後,promise狀態轉化爲rejected,而且執行失敗返回的處理函數。

如今咱們的代碼大概是這樣的:

class Promise {
    constructor(executor) {
        this.status = PENDING 
        this.result = undefined
        executor(data => resolveProvider(this, data), err => rejectProvider(this, err))
    }
}

function resolveProvider(promise, data) {
    if (promise.status !== PENDING) return false
    promise.status = FULFILLED
}
function rejectProvider(promise, data) {
    if (promise.status !== PENDING) return false
    promise.status = FULFILLED
}

Dont Repeat Yourselt!!!咱們能夠看到上面代碼後面兩個函數基本相同,其實咱們能夠把它整合成一個函數,在結合高階函數的使用。

const statusProvider = (promise, status) => data => {
    if (promise.status !== PENDING) return false
    promise.status = status
    promise.result = data
}
class Promise {
    constructor(executor) {
        this.status = PENDING 
        this.result = undefined
        executor(statusProvider(this, FULFILLED), statusProvider(this, REJECTED))
    }
}

如今咱們的代碼就看上去簡潔多了。

第二步:爲Promise添加處理函數

其實經過 new Promise(executor)已經能夠生成一個Promise實例了,甚至咱們能夠經過傳遞到executor中的resolve和reject方法來改變promise狀態,可是!如今的promise依然沒啥卵用!!!由於咱們並無給它添加成功和失敗返回的處理函數。

首先咱們須要給咱們的promise增長兩個屬性,successListener和failureListener用來分別緩存成功處理函數和失敗處理函數。

class Promise {
    constructor(executor) {
        this.status = PENDING
         this.successListener = []
         this.failureListener = []
        this.result = undefined
        executor(statusProvider(this, FULFILLED), statusProvider(this, REJECTED))
    }
}

怎麼添加處理函數呢?ECMASCRIPT標準中說到,咱們能夠經過promise原型上面的then方法爲promise添加成功處理函數和失敗處理函數,能夠經過catch方法爲promise添加失敗處理函數。

const statusProvider = (promise, status) => data => {
    if (promise.status !== PENDING) return false
    promise.status = status
    promise.result = data
    switch(status) {
        case FULFILLED: return promise.successListener.forEach(fn => fn(data))
        case REJECTED: return promise.failurelistener.forEach(fn => fn(data))
    }
}
class Promise {
    constructor(executor) {
        this.status = PENDING
        this.successListener = []
        this.failurelistener = []
        this.result = undefined
        executor(statusProvider(this, FULFILLED), statusProvider(this, REJECTED))
    }
    /**
     * Promise原型上面的方法
     */
    then(...args) {
        switch (this.status) {
            case PENDING: {
                this.successListener.push(args[0])
                this.failurelistener.push(args[1])
                break
            }
            case FULFILLED: {
                args[0](this.result)
                break
            }
            case REJECTED: {
                args[1](this.result)
            }
        }
    }
    catch(arg) {
        return this.then(undefined, arg)
    }
}

咱們如今的Promise基本初具雛形了。甚至能夠運用到一些簡單的場景中了。舉個例子。

/*建立一個延時resolve的pormise*/
new Promise((resolve, reject) => {setTimeout(() => resolve(5), 2000)}).then(data => console.log(data)) // 5
/*建立一個及時resolve的promise*/
new Promise((resolve, reject) => resolve(5)).then(data => console.log(data)) // 5
/*鏈式調用then方法還不可以使用!*/
new Promise(resolve=> resolve(5)).then(data => data).then(data => console.log(data))
// Uncaught TypeError: Cannot read property 'then' of undefined
第三步:Promise的鏈式調用

Promise須要實現鏈式調用,咱們須要再次回顧下then方法的定義:

then方法爲pormise添加成功和失敗的處理函數,同時then方法返回一個新的promise對象,這個新的promise對象resolve處理函數的返回值,或者當沒有提供處理函數時直接resolve原始的值。

能夠看出,promise可以鏈式調用歸功於then方法返回一個全新的promise,而且resolve處理函數的返回值,固然,若是then方法的處理函數自己就返回一個promise,那麼久不用咱們本身手動生成一個promise了。瞭解了這些,就開始動手寫代碼了。

const isPromise = object => object && object.then && typeof object.then === 'function'
const noop = () => {}

const statusProvider = (promise, status) => data => {
    // 同上面代碼
}

class Promise {
    constructor(executor) {
        // 同上面代碼
    }
    then(...args) {
        const child = new this.constructor(noop)

        const handler = fn => data => {
            if (typeof fn === 'function') {
                const result = fn(data)
                if (isPromise(result)) {
                    Object.assign(child, result)
                } else {
                    statusProvider(child, FULFILLED)(result)
                }    
            } else if(!fn) {
                statusProvider(child, this.status)(data)
            }
        }
        switch (this.status) {
            case PENDING: {
                this.successListener.push(handler(args[0]))
                this.failurelistener.push(handler(args[1]))
                break
            }
            case FULFILLED: {
                handler(args[0])(this.result)
                break
            }
            case REJECTED: {
                handler(args[1])(this.result)
                break
            }
        }
        return child
    }
    catch(arg) {
        return this.then(undefined, arg)
    }
}

​ 首先咱們寫了一個isPromise方法,用於判斷一個對象是不是promise。就是判斷對象是否有一個then方法,免責聲明爲了實現上的簡單,咱們不區分thenable和promise的區別,可是咱們應該是知道。全部的promise都是thenable的,而並非全部的thenable對象都是promise。(thenable對象是指帶有一個then方法的對象,該then方法其實就是一個executor。)isPromise的做用就是用於判斷then方法返回值是不是一個promise,若是是promise,就直接返回該promise,若是不是,就新生成一個promise並返回該promise。

​ 因爲須要鏈式調用,咱們對successListener和failureListener中處理函數進行了重寫,並非直接push進去then方法接受的參數函數了,由於then方法須要返回一個promise,因此當then方法裏面的處理函數被執行的同時,咱們也須要對then方法返回的這個promise進行處理,要麼resolve,要麼reject掉。固然,大部分狀況都是須要resolve掉的,只有當then方法沒有添加第二個參數函數,同時調用then方法的promise就是rejected的時候,才須要把then方法返回的pormise進行reject處理,也就是調用statusProvider(child, REJECTED)(data).

toy Promise實現的完整代碼:

const PENDING = 'PENDING' // Promise 的 初始狀態
const FULFILLED = 'FULFILLED' // Promise 成功返回後的狀態
const REJECTED = 'REJECTED' // Promise 失敗後的狀態

const isPromise = object => object && object.then && typeof object.then === 'function'
const noop = () => {}

const statusProvider = (promise, status) => data => {
    if (promise.status !== PENDING) return false
    promise.status = status
    promise.result = data
    switch(status) {
        case FULFILLED: return promise.successListener.forEach(fn => fn(data))
        case REJECTED: return promise.failurelistener.forEach(fn => fn(data))
    }
}

class Promise {
    constructor(executor) {
        this.status = PENDING
        this.successListener = []
        this.failurelistener = []
        this.result = undefined 
        executor(statusProvider(this, FULFILLED), statusProvider(this, REJECTED))
    }
    /**
     * Promise原型上面的方法
     */
    then(...args) {
        const child = new this.constructor(noop)

        const handler = fn => data => {
            if (typeof fn === 'function') {
                const result = fn(data)
                if (isPromise(result)) {
                    Object.assign(child, result)
                } else {
                    statusProvider(child, FULFILLED)(result)
                }    
            } else if(!fn) {
                statusProvider(child, this.status)(data)
            }
        }
        switch (this.status) {
            case PENDING: {
                this.successListener.push(handler(args[0]))
                this.failurelistener.push(handler(args[1]))
                break
            }
            case FULFILLED: {
                handler(args[0])(this.result)
                break
            }
            case REJECTED: {
                handler(args[1])(this.result)
                break
            }
        }
        return child
    }
    catch(arg) {
        return this.then(undefined, arg)
    }
}
4、怎麼讓咱們的toy Promise變強健
  1. 在ECMAScript標準中,Promise構造函數上面還提供了一些靜態方法,好比Promise.resolvePromise.rejectPromsie.allPromise.race。當咱們有了上面的基礎實現後,爲咱們的toy Promise添加上面這些新的功能必定能讓其更加實用。

  2. 在咱們的基本實現中,咱們並無區分thenable對象,其實Promise.resolvethen方法均可以接受一個thenable對象,並把該thenable對象轉化爲一個promise對象,若是想讓咱們的toy Promise用於生產的話,這也是要考慮的。

  3. 爲了讓咱們的toy Promise變得更強壯,咱們須要擁有強健的錯誤處理機制,好比驗證executor必須是一個函數、then方法的參數只能是函數或者undefined或null,又好比executor和then方法中拋出的錯誤並不可以被window.onerror監測到,而只可以經過錯誤處理函數來處理,這也是須要考慮的因素。

  4. 若是咱們的Promise polyfill是考慮支持多平臺,那麼首要考慮的就是瀏覽器環境或Node.js環境,其實在這兩個平臺,原生Promise都是支持兩個事件的。就拿瀏覽器端舉例:

    • unhandledrejection: 在一個事件循環中,若是咱們沒有對promise返回的錯誤進行處理,那麼就會在window對象上面觸發該事件。

    • rejectionhandled:若是在一個事件循環後,咱們纔去對promise返回的錯誤進行處理,那麼就會在window對象上面監聽到此事件。

關於這兩個事件以及node.js平臺上面相似的事件請參考Nicholas C. Zakas新書<understanding es6>

Promise可以很棒的處理異步編程,要想學好它我認爲最好的方法就是親自動手去實現一個本身的Promise,下面的項目Jocs/promise是個人實現,歡迎你們pr和star。

相關文章
相關標籤/搜索