實現一個玩具 Promise ~

sea_ljf發表於2017-11-11

hello~親愛的觀眾老爺們大家好,有段時間沒寫文章了,最近主要忙於工作交接,實在沒騰出時間進行總結。這次為大家帶來 es6 中 Promise 的簡單實現。

事實說類似的文章已經不少,不少大神對此都有精彩的實現。然而自己消化了才是最好的,在看文章中其實還是遇到不少坑,如 this 的指向,回撥的到底是哪個 Promise 等。本文儘量解釋清楚上述問題,力求讓大家更好地掌握 Promise

釋出/訂閱 的簡單實現

JS 的非同步程式設計相信大家都比較熟悉,在 Promise 出現之前,主要是使用 釋出/訂閱 模式、回撥函式等方式實現非同步的。在我的理解中,其實 Promise 特別像 釋出/訂閱 是在使用 釋出/訂閱 模式,在合適的時機呼叫 resolve(),就如釋出事件一樣,訂閱者的回撥函式即被註冊。因而我們先實現一個簡單的 釋出/訂閱 模式:

function Promise(fn) {
  //訂閱者回撥函式陣列
  this.callBacks = [];
  //釋出資訊 呼叫訂閱者的回撥函式 使用箭頭函式可以保證this的指向是此Promise例項
  const resolve = val => {
    //執行回撥
    this.callBacks.forEach(fn => {
      fn(val);
    })
  };
  //將resolve作為實參傳入fn中,交由fn決定何時釋出資訊
  fn(resolve);
}複製程式碼

熟悉 釋出/訂閱 的童鞋,相信看這個程式碼會感覺特別熟悉,不太熟悉的童鞋可以思考這麼個場景:去商城買衣服時,剛好沒了你的碼數,於是你留下的電話(推入 calBacks 陣列中),於是銷售小姐姐有貨的時候通知我回來買(傳入的 fn 呼叫 resolve, 釋出資訊)。典型的 好萊塢原則實現。

上述程式碼還缺少一個銷售小姐姐記錄電話,也就是將一些函式推入 callBacks 的實現,我們可以新增這樣的程式碼:

Promise.prototype.then = function(successCallback) {
  this.callBacks.push(successCallback);
};複製程式碼

嘗試一下玩一下:

const p = new Promise(function(res) {
  setTimeout(() => {
    res(123);
  }, 1000);
});

p.then(function(val) {
  console.log(val);
})複製程式碼

在瀏覽器執行上述程式碼後,可以看到1s後控制檯中列印出 123

新增狀態

上面的通過 釋出/訂閱 實現 Promise 雖然執行良好,然而還是缺了日常使用 Promise 中的狀態:如果去買衣服的時候已經有我的碼數了,那留電話讓銷售小姐姐再打電話我是毫無意義的,肯定是當場買走好了啊。因而我們引入狀態,為了簡單實現,暫時先引入沒碼數的狀態: pending 與可以立即買走的狀態: fulfill

首先修改 then 方法,如果有碼數也就是 fulfill 時立即執行回撥:

Promise.prototype.then = function(successCallback) {
  //如果promise已經決議 那麼久立即執行回撥
  if(this.status === 'fulfill'){
    successCallback();
  }else{
    this.callBacks.push(successCallback);
  }
};複製程式碼

但我們會發現,此時無法將 Promise 決議後的值傳入 successCallback 中,這也好辦,在建構函式中再定義一個 _val 記錄 Promise 決議後的值就好,完整實現如下:

function Promise(fn) {
  //訂閱者回撥函式陣列
  this.callBacks = [];
  //promise的狀態 一開始時必然是pending
  this.status = 'pending';
  //記錄決議後的值 一開始預設為null
  this._val = null;
  //釋出資訊 呼叫訂閱者的回撥函式 使用箭頭函式可以保證this的指向是此Promise例項
  const resolve = val => {
    //切換狀態
    this.status = 'fulfill';
    //決議賦值
    this._val = val;
    //執行回撥
    this.callBacks.forEach(fn => {
      fn(val);
    })
  };
  //將resolve作為實參傳入fn中,交由fn決定何時釋出資訊
  fn(resolve);
}

Promise.prototype.then = function(successCallback) {
  //如果promise已經決議 那麼久立即執行回撥 不然就先推入回撥陣列中
  if (this.status === 'fulfill') {
    successCallback(this._val);
  } else {
    this.callBacks.push(successCallback);
  }
};複製程式碼

測試一下:

const p = new Promise(function(res) {
  setTimeout(() => {
    res(123);
  }, 1000)
});
p.then(function(val) {
  console.log(val);
});

setTimeout(() => {
  p.then(function(val) {
    console.log(val);
  });
}, 2000)複製程式碼

控制檯一秒後列印出 123 ,再隔一秒後再次列印出 123。So far so good~

鏈式呼叫

es6 中,Promise 是可以鏈式呼叫的,而現在還不行。一般而言,為了達成鏈式呼叫,最簡單直接的方法就是在 then 呼叫後 return this;,然而在 Promise 的實現中,這是不折不扣的陷阱。試想一下,如果不斷通過 return this; 來達到鏈式呼叫,那麼該如何管理回撥函式的陣列呢?而根據Promise/A+規範,每次呼叫 then 都會返回一個新的 Promise。通過不斷返回新的 Promise,回撥函式的陣列管理起來就很方便啦!按照這個思路實現看一下:

   function Promise(fn) {
     //訂閱者回撥函式陣列
     this.callBacks = [];
     //promise的狀態 一開始時必然是pending
     this.status = 'pending';
     //記錄決議後的值 一開始預設為null
     this._val = null;
     //釋出資訊 呼叫訂閱者的回撥函式 使用箭頭函式可以保證this的指向是此Promise例項
     const resolve = val => {
       //切換狀態
       this.status = 'fulfill';
       //決議賦值
       this._val = val;
       //執行回撥
       this.callBacks.forEach(fn => {
         fn(val);
       })
     };
     //將resolve作為實參傳入fn中,交由fn決定何時釋出資訊
     fn(resolve);
   }

   Promise.prototype.then = function(successCallback) {
     //新建一個高階函式處理return出去promise的resolve
     const _handleSuccessCallback = resolve => val => {
       //successCallback是Promise例項呼叫then呼叫時傳入的successCallback 不是return出去的Promise例項再呼叫then時候傳入的successCallback哦
       const result = successCallback(val);
       //如果result的值是promise 那麼就在加一個then進去 執行return出去promise的resolve
       if (result && result instanceof Promise) {
         result.then(_val => {
           resolve(_val);
         })
       } else {
         resolve(result);
       }
     };
     //為容易理解起見 快取this
     const that = this;
     return new Promise(function(resolve) {
       //如果promise已經決議 那麼久立即執行回撥 不然就先推入回撥陣列中
       if (that.status === 'fulfill') {
         _handleSuccessCallback(resolve)(that._val);
       } else {
         that.callBacks.push(_handleSuccessCallback(resolve));
       }
     })
   };複製程式碼

雖然我註釋寫了不少,但估計有的同學會覺得比較繞,這版和之前唯一不同是在於 then 返回一個新的 Promise,這個之前解釋過,問題還是不大,難點是在於裡面的 _handleSuccessCallback 方法。

先明確問題, Promise.prototype.then 接受一個函式,它既可以是同步的,處理完之後返回一個值(甚至不返還),也可以是非同步的,返回一個 Promise 例項,在合適的時機呼叫 resolve,非同步傳遞值供後面的 then 使用。同步還好說,關鍵是如何如何解決非同步問題呢?要知道返回的 Promise 例項是可以帶很多 then 的哦!

先解決後一個問題,其實無論帶有多少個 then,根據我們的實現,都是返回最後一個 then 呼叫後返回的 Promise 例項。至於其中如何呼叫,其實我們已經實現好了,這有點像遞迴,處理好邏輯後,讓函式不斷自己呼叫自己就好了。

再來解決第一個問題,其實是通過 _handleSuccessCallback 這個函式實現。_handleSuccessCallback 接受一個引數,就是 Promise 建構函式內的 resolve,但請記住,這個 resolvethen 呼叫後返回的 Promise 例項的 resolve,後稱為返回例項。

傳參呼叫後返回一個新的函式,這個新函式也接受一個引數 val,也就是 Promise 例項中決議了的值,這個例項是呼叫 then 方法的例項,後成為呼叫 then 例項。新函式在兩種情況下會被呼叫,呼叫 then 例項狀態是 fulfill 時,或是呼叫 then 例項從 pending 轉為 fulfill 時呼叫建構函式內的 resolve 方法。

當新函式呼叫時,先將呼叫 then 例項決議後的值作為引數傳給 successCallback 執行,記錄結果為 result。如果 result 是一個 Promise 例項,那就新增一個 then ,將 result 決議後的值傳給返回例項的 resolve 執行即可。如若不是 Promise 例項,那就更好辦了,直接將 result 作為實參傳給返回例項的 resolve 呼叫即可。

至此,兩個問題都解決了,無論傳入 then 中的函式是非同步還是同步的,我們都可以將它呼叫返回的值或決議後的值傳給後面的 then 執行。

寫個小例子測試一下:

const p = new Promise(function(res) {
  setTimeout(() => {
    res(123);
  }, 1000);
});

p.then(function(val) {
  console.log(val);
  return new Promise(function(res) {
    setTimeout(() => {
      res(456);
    }, 1000);
  }).then(val => {
    console.log(val);
    return 789;
  })
}).then(val => console.log(val));複製程式碼

瀏覽器跑一下這個例子,會在1秒後列印123,再1秒後列印出456,然後是789。邏輯整體是沒有問題的。

小結

至此,自己實現的 Promise 算是完成啦,當然這是最粗糙的實現,還缺少很多功能,如 rejectPromise.resolvePromise.reject 等。然而通過參考上述的例子,相信看官大人你肯定很容易就能實現其他功能。由於篇幅關係,就不再實現近似的功能了。

其實瞭解實現還是其次,主要是實現過程中用到的技巧是十分值得學習的。能熟練地非同步程式設計是每一個前端都必須掌握的。感謝各位看官大人看到這裡~希望本文對你有所幫助。謝謝!

參考資料

Node.js 實踐教程 - Promise 實現

30分鐘,讓你徹底明白Promise原理

相關文章