根據 Promises/A+規範 手寫 Promsie

afan發表於2018-07-27

前言

上一篇文章 JavaScript Promise基礎(對於 Promise用法不熟悉的,可以先看這篇文章,理解了再來看這篇文章,會對你有很大幫助)中,介紹了Promise的基本使用,在這篇文章中,我們將根據 Promises/A+ 規範試著自己來寫一個Promise,主要是學習Promise的內部機制與它的程式設計思想。

Promise到底是什麼?

Promise到底是啥玩意呢?是一個類、陣列、物件、函式?好了不猜了,列印出來看看吧console.dir(Promise),是騾子是馬拉出來遛遛唄!

根據 Promises/A+規範 手寫 Promsie

呀呀呀!原來 Promise 是一個建構函式,自己身上有 all、resolve、reject,原型上有 then、catch 等眼熟的方法。那就不用廢話了 new 一個玩玩唄!

let p=new Promise((resolve,reject)=>{
    console.log(1);
    resolve('成功');
    reject('失敗');
});
console.log(2);
p.then((data)=>{
    console.log(data+1);
},(err)=>{
    console.log(err);
})
p.then((data)=>{
    console.log(data+2);
},(err)=>{
    console.log(err);
})
複製程式碼

輸出 1 2 成功1 成功2

new Promise時傳遞一個函式(executor執行器是立即執行的),並接收兩個引數:resolve,reject,分別表示成功的回撥函式與失敗的回撥函式。每一個例項都有一個 then 方法,引數是成功和失敗,成功會有成功的值,失敗會有失敗的原因,並且成功就不能失敗反之也一樣。同一個 Promise 可以多次 then……貌似跑偏了呀!你們都懂也應該會用,還是回到主題開始自己實現吧!

1、實現基本的 Promise

class Promise{
    constructor(executor){
        this.status='pending';  //預設的狀態
        this.value=undefined;   //預設成功的值
        this.reason=undefined;  //預設失敗的原因
        this.onResolvedCallbacks=[];//存放成功的陣列
        this.onRejectedCallbacks=[];//存放失敗的陣列
        let resolve=(value)=>{
            if(this.status==='pending'){//這裡的判 斷是為了防止executor中呼叫兩次resovle或reject方法
               this.status='resolved';//成功了 
                this.value=value;//成功的值
            }
        }
        let reject=(reason)=>{
            if(this.status==='pending'){
                this.status='rejected';//失敗了
                this.reason=reason;//失敗的原因
            }
        }
        try {//捕獲異常
            executor(resolve,reject);//預設讓執行器執行
        } catch (err) {
            reject(err)
        } 
    }
    then(onFufilled,onRejected){    
        if(this.status==='resolved'){
            onFufilled(this.value);
        }
        if(this.status==='rejected'){
            onRejected(this.reason);
        }
    }
}

let p=new Promise((resolve,reject)=>{
    resolve('成功');
    //reject('失敗');
})
p.then((data)=>{
    console.log(data);
},(err)=>{
    console.log(err);
})
複製程式碼
  • executor:例項化 Promise 物件時傳入的引數,即 (resolve,reject)=>{ }
  • status:Promise的狀態,預設為 pendding 態,每當呼叫 resolve 或 reject 方法時,就會改變值,在後面的 then 方法中會用到
  • value:resolve回撥成功的值
  • reason:reject回撥成功的值
  • resolve:成功執行的函式,執行時傳入的引數會作為 then 方法中第一個回撥函式的引數
  • reject:失敗執行的函式,執行時傳入的引數會作為 then 方法中第二個回撥函式的引數

執行輸出成功,那麼問題來了,我們費勁半天寫 Promise 上來就執行有啥用?使用 Promise 時我們一般會寫一些非同步程式碼,等到非同步操作執行完才會觸發 resolve 或者 reject 函式。可是現在當執行 then 方法的時候此時的狀態還是初始的pending 狀態,所以為了能取到引數,我們可以通過釋出訂閱模式來實現。

2、非同步處理

then 方法裡新增如下程式碼,當狀態為 pending 時,我們先把回撥函式存到對應的陣列裡,等待呼叫。

if(this.status==='pending'){
    this.onResolvedCallbacks.push(()=>{
        onFufilled(this.value);
    });
    this.onRejectedCallbacks.push(()=>{
        onRejected(this.reason);
    })
}
複製程式碼

resolve 和 reject 方法裡分別新增如下程式碼,當呼叫的時候,把對應陣列裡的函式依次執行

this.onResolvedCallbacks.forEach(fn=>fn());

this.onRejectedCallbacks.forEach(fn=>fn());

複製程式碼

我們都知道 Promise 有一個最為重要的 then 方法。為了保證鏈式呼叫, then 方法呼叫後返回一個新的 Promise,會將這個 Promise 的值傳遞給下一次 then 中。並且上一次 then 中不管是成功還是失敗或是返回一個普通值,都會傳遞到下一次 then 的引數。

3、實現鏈式 then

首先我們知道,then 是有返回值的。而且可以一直 then 下去,所以之前的 then 必須返回一個新的 Promise。所以我們根據Promises/A+對 then 方法改造如下。

then(onFufilled,onRejected){
    let promise2;
    if(this.status==='resolved'){
        promise2=new Promise((resolve,reject)=>{
            let x=onFufilled(this.value);
            //判斷p是不是一個promise,如果是取它的結果作為promise2成功的結果,
            //如果返回一個普通值,同樣作為promise2成功的結果
            resolvePromise(promise2,x,resolve,reject);//解析p和promise2之間的關係
        });
    }
    if(this.status==='rejected'){
        promise2=new Promise((resolve,reject)=>{
            let x=onRejected(this.reason);
            resolvePromise(promise2,x,resolve,reject);
        })
    }
    if(this.status==='pending'){//當前既沒有成功,也沒有失敗
        promise2=new Promise((resolve,reject)=>{
            this.onResolvedCallbacks.push(()=>{//存放成功的回撥
                let x=onFufilled(this.value);
                resolvePromise(promise2,x,resolve,reject);
            }               
            );
            this.onRejectedCallbacks.push(()=>{//存放失敗的回撥
                let x=onRejected(this.reason);
                resolvePromise(promise2,x,resolve,reject);
            })
        })
    }
    return promise2;//呼叫then後返回一個新的promise
}
複製程式碼

resolvePromise 是幹啥的呢?由於 then 可能返回任意值,所以根據Promises/A+規範對 then 返回的值進行如下處理或解析。

function resolvePromise(promise2,x,resolve,reject){
    //判斷x是不是promise
    //如果當前返回的promise和x引用同一個物件報型別錯誤(不能自己等待自己完成)
    if(promise2===x){
        return reject(new TypeError('迴圈引用'));
    }
    //x不是null並且是物件或函式時,可能是promise
    if(x!==null&&(typeof x==='object'|| typeof x==='function')){
        let called; //標識當前promise有沒有呼叫過
        try{//儘量讓別人瞎寫,防止取then時出現異常
            let then=x.then;//取x的then看是不是函式
            if(typeof then==='function'){//如果是函式就認為它是promise
                then.call(x,(y)=>{//第一個引數是this,後面的是成功的回撥和失敗的回撥
                    if(called) return;
                    called=true;
                    resolvePromise(promise2,y,resolve,reject);//如果y是promise繼續遞迴解析
                },(err)=>{//只要有一個失敗了就失敗了
                    if(called) return;
                    called=true;
                    reject(err);
                })
            }else{//then是一個普通物件直接成功
               resolve(x);
            }
        }catch(e){
            if(called) return;
            called=true;
            reject(e);
        }      
    }else{//如果x是普通值直接成功
        resolve(x);
    }
}
複製程式碼

值的穿透

我們用 Promise 時發現,當不給 then 中傳入引數時,後面的 then 依舊可以得到之前 then 的返回值。例如:p.then().then(),這就是值的穿透。

then(onFufilled,onRejected){
    //解決onFufilled或onRejected沒有傳的問題
    onFufilled=typeof onFufilled==='function'?onFufilled:d=>d;
    onRejected=typeof onRejected==='function'?onRejected:e=>{throw e};
    let promise2;
    if(this.status==='resolved'){
        promise2=new Promise((resolve,reject)=>{
            let x=onFufilled(this.value);
            //判斷p是不是一個promise,如果是取它的結果作為promise2成功的結果,
            //如果返回一個普通值,同樣作為promise2成功的結果
            resolvePromise(promise2,x,resolve,reject);//解析p和promise2之間的關係
        });
    }
    if(this.status==='rejected'){
        promise2=new Promise((resolve,reject)=>{
            let x=onRejected(this.reason);
            resolvePromise(promise2,x,resolve,reject);
        })
    }
    if(this.status==='pending'){//當前既沒有成功,也沒有失敗
        promise2=new Promise((resolve,reject)=>{
            this.onResolvedCallbacks.push(()=>{//存放成功的回撥
                let x=onFufilled(this.value);
                resolvePromise(promise2,x,resolve,reject);
            }               
            );
            this.onRejectedCallbacks.push(()=>{//存放失敗的回撥
                try{
                    let x=onRejected(this.reason);
                    resolvePromise(promise2,x,resolve,reject);
                }catch(e){
                    reject(e);
                }
            })
        })
    }
    return promise2;//呼叫then後返回一個新的promise
}
複製程式碼

executor 執行的時候我們在外面包了 try{}catech 但是我們內部程式碼是非同步的,就無法捕獲錯誤了,需要給每個 then 中的方法都加一個 try{}catch

then(onFufilled,onRejected){
    //解決onFufilled或onRejected沒有傳的問題
    onFufilled=typeof onFufilled==='function'?onFufilled:d=>d;
    onRejected=typeof onRejected==='function'?onRejected:e=>{throw e};
    let promise2;
    if(this.status==='resolved'){
        promise2=new Promise((resolve,reject)=>{
            setTimeout(() => {
                try{
                    let x=onFufilled(this.value);
                    //判斷p是不是一個promise,如果是取它的結果作為promise2成功的結果,
                    //如果返回一個普通值,同樣作為promise2成功的結果
                    resolvePromise(promise2,x,resolve,reject);//解析p和promise2之間的關係  
                }catch(e){
                    reject(e);
                }           
            }, 0);

        });
        // return promise2;
    }
    if(this.status==='rejected'){
        promise2=new Promise((resolve,reject)=>{
            setTimeout(() => {
                try{
                    let x=onRejected(this.reason);
                    resolvePromise(promise2,x,resolve,reject);  
                }catch(e){
                    reject(e);
                }                 
            }, 0);
        })
        // return promise2;
    }
    if(this.status==='pending'){//當前既沒有成功,也沒有失敗
        promise2=new Promise((resolve,reject)=>{
            this.onResolvedCallbacks.push(()=>{//存放成功的回撥
                setTimeout(() => {
                    try{
                        let x=onFufilled(this.value);
                        resolvePromise(promise2,x,resolve,reject);
                    }catch(e){
                        reject(e);
                    }                        
                }, 0);
            });
            this.onRejectedCallbacks.push(()=>{//存放失敗的回撥
                setTimeout(() => {
                    try{
                        let x=onRejected(this.reason);
                        resolvePromise(promise2,x,resolve,reject);
                    }catch(e){
                        reject(e);
                    }                     
                }, 0);
            })
        })
        // return promise2;
    }
    return promise2;//呼叫then後返回一個新的promise
}
複製程式碼

4、Promise 其他方法實現

4.1 catch

catch 接收的引數只有錯誤,也就相當於 then 方法沒有成功的簡寫。而且 catch 後依然可以 then,那就簡單暴力上程式碼吧!

catch(onRejected){
    return this.then(null,onRejected);
}
複製程式碼

4.2 resolve與reject

Promise.resolve()、Promise.reject() 這兩種用法,是直接可以通過類呼叫的,原理就是返回一個內部是resolve 或 reject 的 Promise 物件。

Promise.resolve=function(val){
    return new Promise((resolve,reject)=>{
        resolve(val)
    })
}
Promise.reject=function(val){
    return new Promise((resolve,reject)=>{
        reject(val)
    })
}
複製程式碼

4.3 all

all方法的作用就是將一個陣列的 Promise 物件放在其中,當全部 resolve 的時候就會執行 then 方法,當有一個 reject 的時候就會執行 catch,並且他們的結果也是按著陣列中的順序來的.

Promise.all = function(promises){
  let arr = [];
  let i = 0;
  function processData(index,data){
    arr[index] = data;
    i++;
    if(i == promises.length){
      resolve(arr);
    }
  }
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(data=>{
        processData(i,data);
      },reject)
    }
  })
}
複製程式碼

5、測試

寫了這麼多,到底符不符合Promises/A+規範呢?

//promise的語法糖
Promise.defer=Promise.deferred=function(){
    let dfd={};
    dfd.promise=new Promise((resolve,reject)=>{
        dfd.resolve=resolve;
        dfd.reject=reject;
    })
    return dfd;
}
複製程式碼

安裝promises-aplus-tests用來測試,安裝:npm install promises-aplus-tests -g,測試:promises-aplus-tests + "檔名"。

到這基本就簡單實現了一個自己的 Promise,此時對 Promise 的內部機制與它的程式設計思想有沒有更深入的理解呢?新手可能一臉懵逼,大牛可能一臉蔑視。希望大家都有收穫。寫的不好,有問題歡迎大家在評論區評論指正(還不快去點贊⊙﹏⊙)!!!

程式碼地址

相關文章