實現一個完美符合Promise/A+規範的Promise

yuxiaoliang發表於2018-07-02

原文在我的部落格中:原文地址 如果文章對您有幫助,您的star是對我最好的鼓勵~

簡要介紹:Promise允許我們通過鏈式呼叫的方式來解決“回撥地獄”的問題,特別是在非同步過程中,通過Promise可以保證程式碼的整潔性和可讀性。本文主要解讀Promise/A+規範,並在此規範的基礎上,自己實現一個Promise.

一、Promise的使用

在瞭解Promise規範之前,我們知道主流的高版本瀏覽器已經支援ECMA中的Promise.

建立一個promise例項:

var p=new Promise(function(resolve,reject){
    setTimeout(function(){
       resolve("success")
    },1000);
    console.log("建立一個新的promise");
})
p.then(function(x){
  console.log(x)
})

//輸出:
建立一個新的promise
success
複製程式碼

上述是一個promise的例項,輸出內容為,“建立一個promise”,延遲1000ms後,輸出"success"。

從上述的例子可以看出,promise方便處理非同步操作。此外promise還可以鏈式的呼叫:

var p=new Promise(function(resolve,reject){resolve()});
p.then(...).then(...).then(...)
複製程式碼

此外Promise除了then方法外,還提供了Promise.resolve、Promise.all、Promise.race等等方法。

二、Promise/A+規範

Promise/A+規範擴充套件了早期的Promise/A proposal提案,我們來解讀一下Promise/A+規範。

1.術語

(1)"promise"是一個物件或者函式,該物件或者函式有一個then方法

(2)"thenable"是一個物件或者函式,用來定義then方法

(3)"value"是promise狀態成功時的值

(4)"reason"是promise狀態失敗時的值

我們明確術語的目的,是為了在自己實現promise時,保持程式碼的規範性(也可以跳過此小節)

2.要求

(1)一個promise必須有3個狀態,pending,fulfilled(resolved),rejected當處於pending狀態的時候,可以轉移到fulfilled(resolved)或者rejected狀態。當處於fulfilled(resolved)狀態或者rejected狀態的時候,就不可變。

promise英文譯為承諾,也就是說promise的狀態一旦發生改變,就永遠是不可逆的。

(2)一個promise必須有一個then方法,then方法接受兩個引數:

promise.then(onFulfilled,onRejected)
複製程式碼

其中onFulfilled方法表示狀態從pending——>fulfilled(resolved)時所執行的方法,而onRejected表示狀態從pending——>rejected所執行的方法。

(3)為了實現鏈式呼叫,then方法必須返回一個promise

promise2=promise1.then(onFulfilled,onRejected)
複製程式碼

三、實現一個符合Promise/A+規範的Promise

解讀了Promise/A+規範之後,下面我們來看如何實現一個Promise, 首先構造一個myPromise函式,關於所有變數和函式名,應該與規範中保持相同。

1.v1.0 初始版本myPromise

function myPromise(constructor){
    let self=this;
    self.status="pending" //定義狀態改變前的初始狀態
    self.value=undefined;//定義狀態為resolved的時候的狀態
    self.reason=undefined;//定義狀態為rejected的時候的狀態
    function resolve(value){
        //兩個==="pending",保證了狀態的改變是不可逆的
       if(self.status==="pending"){
          self.value=value;
          self.status="resolved";
       }
    }
    function reject(reason){
        //兩個==="pending",保證了狀態的改變是不可逆的
       if(self.status==="pending"){
          self.reason=reason;
          self.status="rejected";
       }
    }
    //捕獲構造異常
    try{
       constructor(resolve,reject);
    }catch(e){
       reject(e);
    }
}
複製程式碼

同時,需要在myPromise的原型上定義鏈式呼叫的then方法:

myPromise.prototype.then=function(onFullfilled,onRejected){
   let self=this;
   switch(self.status){
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }
}
複製程式碼

上述就是一個初始版本的myPromise,在myPromise裡發生狀態改變,然後在相應的then方法裡面根據不同的狀態可以執行不同的操作。

var p=new myPromise(function(resolve,reject){resolve(1)});
p.then(function(x){console.log(x)})
//輸出1
複製程式碼

但是這裡myPromise無法處理非同步的resolve.比如:

var p=new myPromise(function(resolve,reject){setTimeout(function(){resolve(1)},1000)});

p.then(function(x){console.log(x)})
//無輸出
複製程式碼

2.v2.0基於觀察模式實現

為了處理非同步resolve,我們修改myPromise的定義,用2個陣列onFullfilledArray和onRejectedArray來儲存非同步的方法。在狀態發生改變時,一次遍歷執行陣列中的方法。

function myPromise(constructor){
    let self=this;
    self.status="pending" //定義狀態改變前的初始狀態
    self.value=undefined;//定義狀態為resolved的時候的狀態
    self.reason=undefined;//定義狀態為rejected的時候的狀態
    self.onFullfilledArray=[];
    self.onRejectedArray=[];
    function resolve(value){
       if(self.status==="pending"){
          self.value=value;
          self.status="resolved";
          self.onFullfilledArray.forEach(function(f){
                f(self.value);
                //如果狀態從pending變為resolved,
                //那麼就遍歷執行裡面的非同步方法
          });
        
       }
    }
    function reject(reason){
       if(self.status==="pending"){
          self.reason=reason;
          self.status="rejected";
          self.onRejectedArray.forEach(function(f){
              f(self.reason);
             //如果狀態從pending變為rejected, 
             //那麼就遍歷執行裡面的非同步方法
          })
       }
    }
    //捕獲構造異常
    try{
       constructor(resolve,reject);
    }catch(e){
       reject(e);
    }
}
複製程式碼

對於then方法,狀態為pending時,往陣列裡面新增方法:

myPromise.prototype.then=function(onFullfilled,onRejected){
   let self=this;
   switch(self.status){
      case "pending":
        self.onFullfilledArray.push(function(){
             onFullfilled(self.value)
        });
        self.onRejectedArray.push(function(){
             onRejected(self.reason)
        });
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }
}
複製程式碼

這樣,通過兩個陣列,在狀態發生改變之後再開始執行,這樣可以處理非同步resolve無法呼叫的問題。這個版本的myPromise就能處理所有的非同步,那麼這樣做就完整了嗎?

沒有,我們做Promise/A+規範的最大的特點就是鏈式呼叫,也就是說then方法返回的應該是一個promise。

3.v3.0then方法實現鏈式呼叫

要通過then方法實現鏈式呼叫,那麼也就是說then方法每次呼叫需要返回一個primise,同時在返回promise的構造體裡面,增加錯誤處理部分,我們來改造then方法

myPromise.prototype.then=function(onFullfilled,onRejected){
    let self=this;
    let promise2;
    switch(self.status){
      case "pending":
        promise2=new myPromise(function(resolve,reject){
             self.onFullfilledArray.push(function(){
                try{
                   let temple=onFullfilled(self.value);
                   resolve(temple)
                }catch(e){
                   reject(e) //error catch
                }
             });
             self.onRejectedArray.push(function(){
                 try{
                   let temple=onRejected(self.reason);
                   reject(temple)
                 }catch(e){
                   reject(e)// error catch
                 }
             });
        })
      case "resolved":
        promise2=new myPromise(function(resolve,reject){
            try{
              let temple=onFullfilled(self.value);
              //將上次一then裡面的方法傳遞進下一個Promise的狀態
              resolve(temple);
            }catch(e){
              reject(e);//error catch
            }
        })
        break;
      case "rejected":
        promise2=new myPromise(function(resolve,reject){
            try{
               let temple=onRejected(self.reason);
               //將then裡面的方法傳遞到下一個Promise的狀態裡
               resolve(temple);   
            }catch(e){
               reject(e);
            }
        })
        break;
      default:       
   }
   return promise2;
}
複製程式碼

這樣通過then方法返回一個promise就可以實現鏈式的呼叫:

p.then(function(x){console.log(x)}).then(function(){console.log("鏈式呼叫1")}).then(function(){console.log("鏈式呼叫2")})
//輸出
1
鏈式呼叫1
鏈式呼叫2
複製程式碼

這樣我們雖然實現了then函式的鏈式呼叫,但是還有一個問題,就是在Promise/A+規範中then函式裡面的onFullfilled方法和onRejected方法的返回值可以是物件,函式,甚至是另一個promise。

4.v4.0 then函式中的onFullfilled和onRejected方法的返回值問題

特別的為了解決onFullfilled和onRejected方法的返回值可能是一個promise的問題。

(1)首先來看promise中對於onFullfilled函式的返回值的要求

I)如果onFullfilled函式返回的是該promise本身,那麼會丟擲型別錯誤

II)如果onFullfilled函式返回的是一個不同的promise,那麼執行該promise的then函式,在then函式裡將這個promise的狀態轉移給新的promise III)如果返回的是一個巢狀型別的promsie,那麼需要遞迴。

IV)如果返回的是非promsie的物件或者函式,那麼會選擇直接將該物件或者函式,給新的promise。

根據上述返回值的要求,我們要重新的定義resolve函式,這裡Promise/A+規範裡面稱為:resolvePromise函式,該函式接受當前的promise、onFullfilled函式或者onRejected函式的返回值、resolve和reject作為引數。

下面我們來看resolvePromise函式的定義:

function resolvePromise(promise,x,resolve,reject){
  if(promise===x){
     throw new TypeError("type error")
  }
  let isUsed;
  if(x!==null&&(typeof x==="object"||typeof x==="function")){
      try{
        let then=x.then;
        if(typeof then==="function"){
           //是一個promise的情況
           then.call(x,function(y){
              if(isUsed)return;
              isUsed=true;
              resolvePromise(promise,y,resolve,reject);
           },function(e){
              if(isUsed)return;
              isUsed=true;
              reject(e);
           })
        }else{
           //僅僅是一個函式或者是物件
           resolve(x)
        }
      }catch(e){
         if(isUsed)return;
         isUsed=true;
         reject(e);
      }
  }else{
    //返回的基本型別,直接resolve
    resolve(x)
  }
}
複製程式碼

改變了resolvePromise函式之後,我們在then方法裡面的呼叫也變成了resolvePromise而不是promise。

myPromise.prototype.then=function(onFullfilled,onRejected){
    let self=this;
    let promise2;
    switch(self.status){
      case "pending":
        promise2=new myPromise(function(resolve,reject){
             self.onFullfilledArray.push(function(){
                setTimeout(function(){
                  try{
	                   let temple=onFullfilled(self.value);
	                   resolvePromise(temple)
	                }catch(e){
	                   reject(e) //error catch
	                }
                })
             });
             self.onRejectedArray.push(function(){
                setTimeout(function(){
                   try{
	                   let temple=onRejected(self.reason);
	                   resolvePromise(temple)
	                 }catch(e){
	                   reject(e)// error catch
	               }
                })
             });
        })
      case "resolved":
        promise2=new myPromise(function(resolve,reject){
           setTimeout(function(){
               try{
	              let temple=onFullfilled(self.value);
	              //將上次一then裡面的方法傳遞進下一個Promise狀態
	              resolvePromise(temple);
	            }catch(e){
                  reject(e);//error catch
               }
           })
        })
        break;
      case "rejected":
        promise2=new myPromise(function(resolve,reject){
           setTimeout(function(){
             try{
               let temple=onRejected(self.reason);
               //將then裡面的方法傳遞到下一個Promise的狀態裡
               resolvePromise(temple);   
             }catch(e){
               reject(e);
             }
           })
        })
        break;
      default:       
   }
   return promise2;
}
複製程式碼

這樣就能處理onFullfilled各種返回值的情況。

var p=new Promise(function(resolve,reject){resolve("初始化promise")})
p.then(function(){return new Promise(function(resolve,reject){resolve("then裡面的promise返回值")})}).then(function(x){console.log(x)})

//輸出
then裡面promise的返回值
複製程式碼

到這裡可能有點亂,我們再理一理,首先返回值有兩個:

  • then函式的返回值——>返回一個新promise,從而實現鏈式呼叫

  • then函式中的onFullfilled和onRejected方法——>返回基本值或者新的promise

這兩者其實是有關聯的,onFullfilled方法的返回值可以決定then函式的返回值。

四、檢測是否完全符合Promise/A+規範

npm install -g promises-aplus-tests 
複製程式碼

具體用法請看promise test然後

promises-aplus-tests  myPromise.js
複製程式碼

結果為:

這裡寫圖片描述
說明我們的實現完全符合Promise/A+規範。

完整程式碼的地址

https://github.com/forthealllight/promise-achieve

五、最後補充Typescript實現的Promise/A+規範(可以忽略此節)

interface IConstructor{
  (resolve:IResolve,reject:IReject):void
}
interface IResolve {
    (x:any):void
}
interface IReject {
    (x:any):void
}
function myPromise(constructor:IConstructor):void{
  let self:object=this;
  self.status="pending";
  self.value=undefined;//if pending->resolved
  self.reason=undefined;//if pending->rejected
  self.onFullfilledArray=[];//to deal with async(resolved)
  self.onRejectedArray=[];//to deal with async(rejeced)
  let resolve:IResolve;
  resolve=function(value:any):void{
    //pending->resolved
    if(self.status==="pending"){
      self.status="resolved";
      self.value=value;
      self.onFullfilledArray.forEach(function(f){
        f(self.value);
      })
    }
  }
  let reject:IReject;
  reject=function(reason:any):void{
    if(self.status==="pending"){
      self.status="rejected";
      self.reason=reason;
      self.onRejectedArray.forEach(function(f){
        f(self.reason);
      })
    }
  }
  //According to the definition that the function "constructor" accept two parameters
  //error catch
  try {
    constructor(resolve,reject);
  } catch (e) {
    reject(e);
  }
}
複製程式碼

單純的寫個工具函式,用ts還是有點影響可讀性。

相關文章