用最少的程式碼手工實現一個Promise,5分鐘看懂

windyfancy發表於2019-05-07

Promise簡介

Promise 採用物件導向的方式封裝了回撥函式,可以將回撥金字塔改為平行的鏈式寫法,優雅的解決了回撥地獄,ES7帶來了非同步的終級解決方案async/await,可以用寫同步程式碼的方式編寫非同步程式碼,而Promise正是async/await的基石。

Promise 是一種設計模式,也是規範,歷史上曾經出現過Promise A/Promise A+/Promise B/Promise D四種規範,最終ES6選擇了Promise A+的方案,真理來之不易。

Promise表面上看起來比較簡單,你看,生成的Promise物件很純淨,只有then,catch,finally幾個方法,還有兩個隱藏的屬性:PromiseStatus和PromiseValue分別表示狀態和返回值

用最少的程式碼手工實現一個Promise,5分鐘看懂

說到Promise狀態,眾所周知,只有pending,fulfilled,rejected 3種狀態,而且不可逆,不成功便成仁,從建立例項時的pending,呼叫成功resolve就變成fulfilled,失敗則變為rejected,整個模型非常簡單

用最少的程式碼手工實現一個Promise,5分鐘看懂
但是深入瞭解,Promise還有許多的潛規則,要深入理解一項技術最好的辦法是造一個輪子。

用最少的程式碼手工實現一個Promise,5分鐘看懂
Promise能夠一統江湖成為非同步的終極解決方案(配合async/await),它的價值絕對是不可估量的,值得你親手實現不止一遍。

極簡版Promise

需求分析:

萬丈高樓平地起,一磚一瓦靠自己。我們先從最簡單的核心功能開始,第一步僅實現Promise的構造器函式和then方法 兩個功能

功能清單:

  1. Promise 構造器函式,傳入一個函式,該函式立即執行,並且有resolve和reject兩個引數,resolve被呼叫時Promise狀態變為fulfilled
  2. 實現then方法,傳入一個函式,該該數在Promise被fulfilled時執行

程式碼實現:

class PromiseA {
  constructor(init) {
    this.PromiseStatus = 'pending';
    var resolve=(val)=>{
        if(this.resolveCallback){
            this.PromiseStatus="fulfilled"
            this.resolveCallback(val);
        }
    }
    if(init){
        init(resolve,reject);
    }
  }
  then(onFulfill,onReject) {
    this.resolveCallback=onFulfill;
    this.rejectCallback=onReject;
    return this;
  }
}
複製程式碼

就這麼簡單,花幾分鐘就可以寫好,寫個測試程式碼跑一下

new PromiseA(function (resolve){
    setTimeout(function (){
        resolve("hello,from promise 1");
    },2000)
}).then(function (msg){
    console.log(msg);
})
複製程式碼

兩秒後輸出了:hello,from promise 1

用最少的程式碼手工實現一個Promise,5分鐘看懂
完美執行,能夠轉得動的輪子就是好輪子!

但是,好像還缺了點什麼?畢竟我們想做的是賓士車的輪子......

完整版Promise

需求分析

上一步我們做出了第一個能執行起來的Promise,但是還缺失一些必備功能,如下:

    1. 每次呼叫then方法應該返回一個新的Promise物件
    1. then方法支援鏈式呼叫,鏈式呼叫有兩種用法:
  •    2.1 then註冊的onFulfill函式沒有返回值,則之後的then全部一起觸發
    複製程式碼
  •    2.2 then註冊的onFulfill函式返回了新的promise,則等這個新的promise fulfill之後,再觸發之後的then
    複製程式碼
    1. reject函式,以及catch方法

實現思路:

精簡版的Promise 很容易實現和讀懂,但是要實現鏈式呼叫什麼的,就有點燒腦了,因為鏈式呼叫本身是連結串列的資料結構,又是高階函式傳來傳去,很容易繞暈,我是花了很久時間除錯修改,實現思路也是在除錯過程中才慢慢理清的,雖然只有幾行程式碼,但是用語言描述比較晦澀難懂,你非得單步除錯一下才能明白其中的奧妙。

  1. 首先,在then方法中返回一個新的promise不是什麼難事,new一下就可以了,但是then方法如果返回了promise,要用新的promise替代,問題是then中的promise已經先返回了,這是先有雞還是先有蛋的問題,時光不能倒流,那只有通過引用傳遞,改寫之前返回的promise了,其實也不用完全替換,只需要改寫resolve回撥就可以了。
  2. 對於then方法中不返回promise的情況,複製原promise的resolve回撥,就可以同時一起觸發多個then回撥

完整程式碼實現:

class PromiseA {
  constructor(init) {
    this.PromiseStatus = 'pending';
    this.PromiseValue=null;
    this.resolveCallback=null;
    this.rejectCallback=null;
    var resolve=(val)=>{
        if(this.resolveCallback){
            this.PromiseValue=val;
            this.PromiseStatus="fulfilled"
            var promiseNew=this.resolveCallback(val);
            if(this.nextPromise){
                let next=this.nextPromise;
                if(promiseNew){ //then方法返回了新的promie,
                    promiseNew.resolveCallback=next.resolveCallback;
                    promiseNew.rejectCallback=next.rejectCallback;
                }else if(next.resolveCallback!=this.resolveCallback){ //沒有返回新的promise,需要防重複呼叫
                    next.resolveCallback(val);
                }
            }
        }
    }
    var reject=(val)=>{
        if(this.rejectCallback){
            this.PromiseStatus="rejected"
            this.rejectCallback(val);
        }
    }
    if(init){
        init(resolve,reject);
    }
  }
  then(onFulfill,onReject) {
    this.resolveCallback=onFulfill;
    this.rejectCallback=onReject;
    var promise=new PromiseA();//建立一個新的promise例項
    promise.resolveCallback=onFulfill;//新的promise例項的resolve函式預設指向當前promise,用來支援多次呼叫then
    this.nextPromise=promise;//儲存一下新的promise引用,便於鏈式呼叫
    return promise;
  }
  catch(onRejected){
    return this.then(null, onRejected);
  }
}
複製程式碼

寫個測試用例跑一下:


console.time("timer1");  
console.time("timer2");  

new PromiseA(function (resolve){
    setTimeout(function (){
        resolve("hello,from promise 1");
        
    },2000)
}).then(function (msg){
    console.log(msg);
    console.timeEnd("timer1");

}).then(function (msg){
    console.log(msg)
    console.timeEnd("timer2");
})
複製程式碼

執行後,兩個then回撥在2秒後同時觸發,說明第一種鏈式呼叫驗證成功

用最少的程式碼手工實現一個Promise,5分鐘看懂

再測試一下第二種鏈式呼叫,測試程式碼如下:

console.time("timer1");  
console.time("timer2");  

new PromiseA(function (resolve){
    setTimeout(function (){
        resolve("hello,from promise 1");
        
    },2000)
}).then(function (msg){
    console.log(msg);
    console.timeEnd("timer1");
    return new PromiseA(function (resolve){
        setTimeout(function (){
            resolve("world,from promise 2")
        },3000)
    })
}).then(function (msg){
    console.log(msg)
    console.timeEnd("timer2");
})
複製程式碼

驗證成功,在2秒後觸發了第一個then回撥,並接收到了hello,from promise1的返回值,在5秒後觸發了第二個then回撥,並接收到了"world,from promise2"的返回值

用最少的程式碼手工實現一個Promise,5分鐘看懂

實現Promise.all和Promise.race

Promise的例項功能已經完工了,翻翻看Promise構造器函式上還有兩個類方法all和race,其中Promise.all是一個非常有用的功能,可以併發執行多個非同步任務,全部成功後再執行resolve,無論是處理多個http並行請求,還是並行執行sql指令碼等平行計算任務,都十分方便。

有了上面的PromiseA類基礎設施,實現這個功能簡直不要太簡單。這次要用靜態方法,也叫類方法,就是在PromiseA構造器函式上定義的,es6 的class 中定義的方法預設是生成在例項的原型中的,加一個static關鍵字就可以變為靜態方法。

實現思路:

  1. 生成一個新的Promise。
  2. 遍歷傳入的promise陣列,依次呼叫每一個promise的then方法註冊回撥。
  3. 在then 回撥中把promise返回值push到一個結果陣列中,檢測結果陣列長度與promise陣列長度相等時表示所有promise都已經resolve了,再執行總的resolve。

Promise.race則更簡單,只有任意一個promise fulfilled就執行總的resolve。

程式碼如下:

static all(list){
      return new PromiseA(function (resolve){
        var results=[];
        list.forEach((promise)=>{
            promise.then((val)=>{
                results.push(val);
                if(results.length==list.length){
                    resolve(results);
                    
                }
            })
        })
      })
  }
  
  static race(list){
    return new PromiseA(function (resolve){
        list.forEach((promise)=>{
            promise.then((val)=>{
                resolve(val);
            })
        })
    });
  }
複製程式碼

小結

盤點了一下,一共用了60多行程式碼,實現了Promise A+的絕大部分功能,除了一些語法糖和一些容錯處理沒有做,業務上能用到的所有功能都已經實現,全部程式碼都已通過測試可以直接執行,如果要做IE瀏覽器下的promise polyfill也是完全夠用的,為了增加可讀性,本文的程式碼用了es6 class和箭頭函式,稍做修改就可以改寫為相容es3標準的程式碼。

最後,推廣一下個人的開源專案,node.js web開發框架: webcontext

傳送門:github.com/windyfancy/…

相關文章