啥?喝着闊落吃着西瓜就把Promise手寫出來了???

前言

雖然今年已經18年,可是今天仍是要繼續聊聊ES6的東西,ES6已通過去幾年,但是咱們對於ES6的語法到底是掌握了什麼程度,是瞭解?會用?仍是精通?相信你們和我同樣都對本身有着一個提高的心,對於新玩具可不能僅僅瞭解,對於其中的思想纔是最吸引人的,因此接下來會經過一篇文章,來讓你們對於Promise這個玩具作到精通的程度!!!npm

打開一瓶冰闊落~~~編程

Promise

Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最先提出和實現,ES6將其寫進了語言標準,統一了用法,原生提供了 Promise對象。

~promise

首先,咱們經過字面能夠看出來Pormise是一種解決方案,並且還有兩種傳統的解決方案·回調函數事件,ok,那麼咱們就來先聊聊這兩種方案。app

回調函數 Callback

回調函數想必你們都不陌生,就是咱們常見的把一個函數當作參數傳遞給另一個函數,在知足了必定的條件以後再去執行回調,好比咱們想要實現一個在三秒後去計算1到5的和,那麼:dom

// 求和函數
    function sum () {
        return eval([...arguments].join('+'))
    }
    // 三秒後執行函數
    function asycnGetSum (callback) {
        setTimeout(function(){
            var result = callback(1,2,3,4,5);
            console.log(result)
        },3000)
    }
    asyncGetSum(sum);

這樣的實現就是回調函數,可是若是我要實如今一段動畫,動畫的執行過程是小球先向右移動100px,而後再向下移動100px,在向左移動100px,每段動畫持續時間都是3s.異步

dom.animate({left:'100px'},3000,'linear',function(){
        dom.animate({top:'100px'},3000,'linear',function(){
            dom.animate({left:'0px'},3000,'linear',function(){
                console.log('動畫 done')
            })
        })
    })

這樣就會看到造成了一個回調嵌套,也就是咱們常說的回調地獄,致使代碼可讀性十分差。async

事件

事件處理就是jQuery中的on綁定事件和trigger觸發事件,其實就是咱們常見的發佈訂閱模式,當我訂閱了一個事件,那麼我就是訂閱者,若是發佈者發佈了數據以後,那麼我就要收到相應的通知。異步編程

// 定義一個發佈中心
    let publishCenter = {
        subscribeArrays:{}, // 定義一個訂閱者回調函數callback
        subscribe:function(key,callback){
            // 增長訂閱者
            if(!this.subscribeArrays[key]){
                this.subscribeArrays[key] = [];
            }
            this.subscribeArrays[key].push(callback)
        },
        publish:function(){
            //發佈 第一個參數是key
            let params = [...arguments];
            let key = params.shift();
            let callbacks = this.subscribeArrays[key];
            if(!callbacks || callbacks.length === 0){
                // 若是沒人訂閱 那麼就返回
                return false
            }
            for( let i = 0 ; i < callbacks.length; i++ ){
                callbacks[i].apply( this, params );
            }
        }
    };
    
    // 訂閱 一個wantWatermelon事件
    publishCenter.subscribe('wantWatermelon',function(){console.log('恰西瓜咯~~')})
    
    //觸發wantWatermelon事件 好咯 能夠看到 恰西瓜咯
    publishCenter.publish('wantWatermelon')

恰西瓜中~~~函數

Promise A+

嗝~ok,吃完咱們進入正題,看到上面異步編程如此如此如此麻煩,對於我這種頭大用戶,固然是拒絕的啊,還好咱們有PormisePormise大法好),下面咱們就來經過實現一個Promise去更深的瞭解Promise的原理,首先咱們瞭解一下PromiseA+,它是一種規範,用來約束你們寫的Promise方法的,爲了讓你們寫的Promise杜絕一些錯誤,按照咱們所指望的流程來走,所以就出現了PromiseA+規範。測試

Promise特色

咱們根據PromiseA+文檔來一步一步的看Promise有什麼特色。

首先咱們看文檔的2.1節,題目是Promise states,也就是說講的是Promise的狀態,那麼都說了些什麼呢,咱們來看一哈:

  • 一個promise只有三種狀態,pending態,fulfilled態(完成態),rejected(拒絕態)
  • 當promise處於pending態時,可能轉化成fulfilled或者rejected
  • 一旦promise的狀態改爲了fulfilled後,狀態就不能再改變了,而且須要提供一個不可變的value
  • 一旦promise的狀態改爲了rejected後,狀態就不能再改變了,而且須要提供一個不可變的reason

ok,那麼咱們就開始寫咱們本身的Promise,咱們先看看一段正常Promise的寫法

// 成功或者失敗是須要提供一個value或者reason
    let promise1 = new Promise((resolve,rejected)=>{
        // 能夠發現 當咱們new Promise的時候這句話是同步執行的 也就是說當咱們初始化一個promise的時候 內部的回調函數(一般咱們叫作執行器executor)會當即執行
        console.log('hahahha');
        // promise內部支持異步
        setTimeout(function(){
            resolve(123);
        },100)
        // throw new Error('error') 咱們也能夠在執行器內部直接拋出一個錯誤 這時promise會直接變成rejected態
    })

根據咱們上面的代碼還有PromiseA+規範中的狀態說明,咱們能夠知道Promise已經有了下面幾個特色

  1. promise有三種狀態 默認pendingpending能夠變成fulfilled(成功態)或者rejected(失敗態),而一旦轉變以後就不能在變成其餘值了
  2. promise內部有一個value 用來存儲成功態的結果
  3. promise內部有一個reason 用來存儲失敗態的緣由
  4. promise接受一個executor函數,這個函數有兩個參數,一個是resolve方法,一個是reject方法,當執行resolve時,promise狀態改變爲fulfilled,執行reject時,promise狀態改變爲rejected
  5. 默認 new Promise 執行的時候內部的executor函數執行
  6. promise內部支持異步改變狀態
  7. promise內部支持拋出異常,那麼該promise的狀態直接改爲rejected

咱們接下來繼續看PromiseA+文檔:

  • promise必需要有一個then方法,用來訪問它當前的value或者是reason
  • 該方法接受兩個參數onFulfilled(成功回掉函數),onRejected(失敗回調函數) promise.then(onFulfilled, onRejected)
  • 這兩個參數都是可選參數,若是發現這兩個參數不是函數類型的話,那麼就忽略 好比 promise.then().then(data=>console.log(data),err=>console.log(err)) 就能夠造成一個值穿透
  • onFulfilled必須在promise狀態改爲fulfilled以後改爲調用,而且呢promise內部的value值是這個函數的參數,並且這個函數不能重複調用
  • onRejected必須在promise狀態改爲rejected以後改爲調用,而且呢promise內部的reason值是這個函數的參數,並且這個函數不能重複調用
  • onFulfilledonRejected這兩個方法必需要在當前執行棧的上下文執行完畢後再調用,其實就是事件循環中的微任務(setTimeout是宏任務,有必定的差別)
  • onFulfilledonRejected這兩個方法必須經過函數調用,也就是說 他們倆不是經過this.onFulfilled()或者this.onRejected()調用,直接onFulfilled()或者onRejected()
  • then方法能夠在一個promise上屢次調用,也就是咱們常見的鏈式調用
  • 若是當前promise的狀態改爲了fulfilled那麼就要按照順序依次執行then方法中的onFulfilled回調
  • 若是當前promise的狀態改爲了rejected那麼就要按照順序依次執行then方法中的onRejected回調
  • then方法必須返回一個promise(接下來咱們會把這個promise稱作promise2),相似於 promise2 = promise1.then(onFulfilled, onRejected);
  • 若是呢onFulfilled()或者onRejected()任一一個返回一個值x,那麼就要去執行resolvePromise這個函數中去(這個函數是用來處理返回值x遇到的各類值,而後根據這些值去決定咱們剛剛then方法中onFulfilled()或者onRejected()這兩個回調返回的promise2的狀態)
  • 若是咱們在then中執行onFulfilled()或者onRejected()方法時產生了異常,那麼就將promise2用異常的緣由ereject
  • 若是onFulfilled或者onRejected不是函數,而且promise的狀態已經改爲了fulfilled或者rejected,那麼就用一樣的value或者reason去更新promise2的狀態(其實這一條和第三條一個道理,也就是值得穿透問題)

好吧,咱們總結了這麼多規範特色,那麼咱們就用這些先來練練手

/**
     * 實現一個PromiseA+
     * @description 實現一個簡要的promise
     * @param {Function} executor 執行器
     * @author Leslie
     */
    function Promise(executor){
        let self = this;
        self.status = 'pending'; // 存儲promise狀態 pending fulfilled rejected.
        self.value = undefined; // 存儲成功後的值
        self.reason = undefined; // 記錄失敗的緣由
        self.onfulfilledCallbacks = []; //  異步時候收集成功回調
        self.onrejectedCallbacks = []; //  異步時候收集失敗回調
        function resolve(value){
            if(self.status === 'pending'){
                self.status = 'fulfilled';// resolve的時候改變promise的狀態
                self.value = value;//修改爲功的值
                // 異步執行後 調用resolve 再把存儲的then中的成功回調函數執行一遍
                self.onfulfilledCallbacks.forEach(element => {
                    element()
                });
            }
        }
        function reject(reason){
            if(self.status === 'pending'){
                self.status = 'rejected';// reject的時候改變promise的狀態
                self.reason = reason; // 修改失敗的緣由
                // 異步執行後 調用reject 再把存儲的then中的失敗回調函數執行一遍
                self.onrejectedCallbacks.forEach(element => {
                    element()
                });
            }
        }
        // 若是執行器中拋出異常 那麼就把promise的狀態用這個異常reject掉
        try {
            //執行 執行器
            executor(resolve,reject);
        } catch (error) {
            reject(error)
        }
    }

    Promise.prototype.then = function(onfulfilled,onrejected){
        // onfulfilled then方法中的成功回調
        // onrejected then方法中的失敗回調
        let self = this;
        // 若是onfulfilled不是函數 那麼就用默認的函數替代 以便達到值穿透
        onfulfilled = typeof onfulfilled === 'function'?onfulfilled:val=>val;
        // 若是onrejected不是函數 那麼就用默認的函數替代 以便達到值穿透
        onrejected = typeof onrejected === 'function'?onrejected: err=>{throw err}
        let promise2 = new Promise((resolve,reject)=>{
            if(self.status === 'fulfilled'){
                // 加入setTimeout 模擬異步
                // 若是調用then的時候promise 的狀態已經變成了fulfilled 那麼就調用成功回調 而且傳遞參數爲 成功的value
                setTimeout(function(){
                    // 若是執行回調發生了異常 那麼就用這個異常做爲promise2的失敗緣由
                    try {
                        // x 是執行成功回調的結果
                        let x = onfulfilled(self.value);
                        // 調用resolvePromise函數 根據x的值 來決定promise2的狀態
                        resolvePromise(promise2,x,resolve,reject);
                    } catch (error) {
                        reject(error)
                    }
                },0)
                
            }
        
            if(self.status === 'rejected'){
                // 加入setTimeout 模擬異步
                // 若是調用then的時候promise 的狀態已經變成了rejected 那麼就調用失敗回調 而且傳遞參數爲 失敗的reason
                setTimeout(function(){
                    // 若是執行回調發生了異常 那麼就用這個異常做爲promise2的失敗緣由
                    try {
                        // x 是執行失敗回調的結果
                        let x = onrejected(self.reason);
                         // 調用resolvePromise函數 根據x的值 來決定promise2的狀態
                        resolvePromise(promise2,x,resolve,reject);
                    } catch (error) {
                        reject(error)
                    }
                    
                },0)
            }
        
            if(self.status === 'pending'){
                //若是調用then的時候promise的狀態仍是pending,說明promsie執行器內部的resolve或者reject是異步執行的,那麼就須要先把then方法中的成功回調和失敗回調存儲襲來,等待promise的狀態改爲fulfilled或者rejected時候再按順序執行相關回調
                self.onfulfilledCallbacks.push(()=>{
                    //setTimeout模擬異步
                    setTimeout(function(){
                        // 若是執行回調發生了異常 那麼就用這個異常做爲promise2的失敗緣由
                        try {
                             // x 是執行成功回調的結果
                            let x = onfulfilled(self.value)
                            // 調用resolvePromise函數 根據x的值 來決定promise2的狀態
                            resolvePromise(promise2,x,resolve,reject);
                        } catch (error) {
                            reject(error)
                        }
                    },0)
                })
                self.onrejectedCallbacks.push(()=>{
                    //setTimeout模擬異步
                    setTimeout(function(){
                        // 若是執行回調發生了異常 那麼就用這個異常做爲promise2的失敗緣由
                        try {
                             // x 是執行失敗回調的結果
                            let x = onrejected(self.reason)
                             // 調用resolvePromise函數 根據x的值 來決定promise2的狀態
                            resolvePromise(promise2,x,resolve,reject);
                        } catch (error) {
                            reject(error)
                        }
                    },0)
                })
            }
        })
        return promise2;
    }

一鼓作氣,是否是以爲以前總結出的特色十分有效,對着特色十分順暢的就擼完了代碼~

那麼就讓咱們接着來看看promiseA+文檔裏還有些什麼內容吧

  • resolvePromise這個函數呢會決定promise2用什麼樣的狀態,若是x是一個普通值,那麼就直接採用x,若是x是一個promise那麼就將這個promise的狀態當成是promise2的狀態
  • 判斷若是xpromise2是一個對象,即promise2 === x,那麼就陷入了循環調用,這時候promise2就會以一個TypeErrorreason轉化爲rejected
  • 若是x是一個promise,那麼promise2就採用x的狀態,用和x相同的valueresolve,或者用和x相同的reasonreject
  • 若是x是一個對象或者是函數 那麼就先執行let then = x.then
  • 若是x不是一個對象或者函數 那麼就resolve 這個x
  • 若是在執行上面的語句中報錯了,那麼就用這個錯誤緣由去reject promise2
  • 若是then是一個函數,那麼就執行then.call(x,resolveCallback,rejectCallback)
  • 若是then不是一個函數,那麼就resolve這個x
  • 若是xfulfilled態 那麼就會走resolveCallback這個函數,這時候就默認把成功的value做爲參數y傳遞給resolveCallback,即y=>resolvePromise(promise2,y),繼續調用resolvePromise這個函數 確保 返回值是一個普通值而不是promise
  • 若是xrejected態 那麼就把這個失敗的緣由reason做爲promise2的失敗緣由reject出去
  • 若是resolveCallback,rejectCallback這兩個函數已經被調用了,或者屢次被相同的參數調用,那麼就確保只調第一次,剩下的都忽略掉
  • 若是調用then拋出異常了,而且若是resolveCallback,rejectCallback這兩個函數已經被調用了,那麼就忽略這個異常,不然就用這個異常做爲promise2reject緣由

咱們又又又又又又總結了這麼多,好吧不說了總結多少就開擼吧。

/**
 * 用來處理then方法返回結果包裝成promise 方便鏈式調用
 * @param {*} promise2 then方法執行產生的promise 方便鏈式調用
 * @param {*} x then方法執行完成功回調或者失敗回調後的result
 * @param {*} resolve 返回的promise的resolve方法 用來更改promise最後的狀態
 * @param {*} reject 返回的promise的reject方法 用來更改promise最後的狀態
 */
function resolvePromise(promise2,x,resolve,reject){
    // 首先判斷x和promise2是不是同一引用 若是是 那麼就用一個類型錯誤做爲Promise2的失敗緣由reject
    if( promise2 === x) return reject(new TypeError('typeError:大佬,你循環引用了!'));
    // called 用來記錄promise2的狀態改變,一旦發生改變了 就不容許 再改爲其餘狀態
    let called;
    if( x !== null && ( typeof x === 'object' || typeof x === 'function')){
        // 若是x是一個對象或者函數 那麼他就有多是promise 須要注意 null typeof也是 object 因此須要排除掉
        //先得到x中的then 若是這一步發生異常了,那麼就直接把異常緣由reject掉
        try {
            let then = x.then;//防止別人瞎寫報錯
            if(typeof then === 'function'){
                //若是then是個函數 那麼就調用then 而且把成功回調和失敗回調傳進去,若是x是一個promise 而且最終狀態時成功,那麼就會執行成功的回調,若是失敗就會執行失敗的回調若是失敗了,就把失敗的緣由reject出去,作爲promise2的失敗緣由,若是成功了那麼成功的value時y,這個y有可能仍然是promise,因此須要遞歸調用resolvePromise這個方法 直達返回值不是一個promise
                then.call(x,y => {
                    if(called) return;
                    called = true;
                    resolvePromise(promise2,y,resolve,reject)
                }, error=>{
                    if(called) return
                    called = true;
                    reject(error)
                })
            }else{
                resolve(x)
            }
        } catch (error) {
            if(called) return
            called = true;
            reject(error)
        }
    }else{
        // 若是是一個普通值 那麼就直接把x做爲promise2的成功value resolve掉
        resolve(x)
    }

}

finnnnnnnnnally,咱們終於經過咱們的不懈努力實現了一個基於PromiseA+規範的Promise

最後呢爲了完美,咱們還要在這個promise上實現Promise.resolve,Promise.reject,以及catchPromise.allPromise.race這些方法。

Promise的一些方法

Promise.resolve = function(value){
    return new Promise((resolve,reject)=>{
        resolve(value)
    })
}
Promise.reject = function(reason){
    return new Promise((resolve,reject)=>{
        reject(reason)
    })
}
Promise.prototype.catch = function(onRejected){
    return this.then(null,onRejected)
}
Promise.all = function(promises){
    return new Promise((resolve,reject)=>{
        let arr = [];
        let i = 0;
        function getResult(index,value){
            arr[index] = value;
            if(++i == promises.length) {
                resolve(arr)
            }
        }
        for(let i = 0;i<promises.length;i++){
            promises[i].then(data=>{
                getResult(i,data)
            },reject)
        }
    })
}
Promise.race = function(promises){
    return new Promise((resolve,reject)=>{
        for(let i = 0 ; i < promises.length ; i++){
            promises[i].then(resolve,reject)
        }
    })
}

Promise 語法糖

恰完西瓜來口糖,語法糖是爲了讓咱們書寫promise的時候可以更加的快速,因此作了一層改變,咱們來看一個例子,好比當咱們封裝一個異步讀取圖片的寬高函數

// 原來的方式
    let getImgWidthHeight = function(imgUrl){
        return new Promise((resolve,reject)=>{
            let img = new Image();
            img.onload = function(){
                resolve(img.width+'-'+img.height)
            }
            img.onerror = function(e){
                reject(e)
            }
            img.src = imgUrl;
        })
    }

是否是以爲怎麼寫起來有點舒服但又有點不舒服,好像我每次都要去寫執行器啊!爲何!好的,沒有爲何,既然不舒服 咱們就改!

// 實現一個promise的語法糖
Promise.defer = Promise.deferred = function (){
    let dfd = {};
    dfd.promise = new Promise((resolve,reject)=>{
        dfd.resolve = resolve;
        dfd.reject = reject;
    })
    return dfd
}

有了上面的語法糖咱們再看一下那個圖片的函數怎麼寫

let newGetImgWidthHeight = function(imgUrl){
        let dfd = Promise.defer();
        let img = new Image();
        img.onload = function(){
            dfd.resolve(img.width+'-'+img.height)
        }
        img.onerror = function(e){
            dfd.reject(e)
        }
        img.url = imgUrl;
        return dfd.promise
    }

是否是發現咱們少了一層函數嵌套,呼 得勁

最終檢測

npm install promises-aplus-tests -g

既然咱們都說了咱們是遵循promiseA+規範的,那至少要拿出點證據來是否是,否則是否是說服不了你們,那麼咱們就用promises-aplus-tests這個包來檢測咱們寫的promise究竟怎麼樣呢!安裝完成以後來跑一下咱們的promise

最終跑出來咱們所有經過測試!酷!晚餐再加個雞腿~

相關文章
相關標籤/搜索