最簡單!實現Promise的兩種思路分析

引言

Promise是一種異步編程的解決方案,經過鏈式調用的方式解決回調地獄。做爲前端面試中的考點,也是前端的基本功,掌握其原理是很是重要的。本次分享就從Promise的使用方式上出發,一步一步剖析其原理,最後幫助你們封裝出本身的Promise前端

注:若是你還不瞭解Promise,建議點擊這裏學習Promise的基本使用語法。es6

本文知識點:面試

  • 非規範的(簡單粗暴)Promise的原理
  • 規範的(Promises/A+Promise的原理

正文


知其然才能知其因此然,咱們先來看一下最常使用的例子,分析有什麼特徵。編程

使用例子

熟悉 Promise使用的人都知道,當調用 getNews()返回一個新的 Promise時,裏面的異步調用操做將會 馬上執行,而後在異步回調裏調用 resolve方法和 reject方法改變 Promise的狀態,執行對應的 then或者 catch函數。

以上代碼有如下幾個特徵:segmentfault

  1. Promise是一個構造函數,其接受一個函數做爲參數。
  2. 做爲參數的函數裏,有兩個方法resolvereject
  3. Promise帶有方法thencatch

resolvereject是怎麼來的?Promise實例化的時候都作了什麼?數組

別急,所謂生死看淡,不服就幹。在回答這兩個問題以前,咱們能夠先直接嘗試構建本身的Promisepromise

開始構建

  1. 構造函數

fn就是咱們使用時傳入的回調函數。

  1. resolvereject

那麼fn是何時調用的呢?其實,在Promise實例初始化的時候內部就自動調用了,而且傳入了內部的resolvereject方法給開發者調用,就像下面這樣:bash

至此,第一個問題獲得了回答,即: resolverejectPromise 內部提供給開發者的。

  1. 添加thencatch方法

這兩個方法是Promise實例的方法,所以應該寫在this或者prototype上。異步

到這裏,一個 Promise基本的骨架就出來了,下面咱們仔細嘮嘮這4個函數的具體做用。

做用分析

  1. resolvereject

想象一下咱們平常使用Promise的場景,在異步請求以後,是須要咱們手動調用resolvereject方法去改變狀態的。編輯器

resolve調用意味着異步請求已經有告終果,能夠執行 then裏面的回調了( reject同理,異步請求失敗時候執行 catch函數。)

  1. thencatch

調用上述的函數時以下:

咱們知道,在resolve被調用前,thencatch函數裏面的回調是不會執行的。那麼咱們這樣寫的時候,它作了什麼呢?

實際上,Promise悄悄把咱們寫的回調函數保存了起來,等到咱們手動調用resolve或者reject時才依次去執行。也就是說,Promise裏的thencatch的做用就是:註冊回調函數,先把一系列的回調函數存起來,等到開發者調用的時候纔拿出來執行。

因此,thencatch 函數應該長這樣:

resolvereject就是分別去調用他們而已。

到目前爲止能夠回答第二個問題了: Promise初始化時,內部調用了咱們傳入的函數,並將 resolvereject方法做爲參數提供。在以後調用的 then或者 catch方法裏,把回調函數保存在內部的一個隊列中,等待狀態改變時候調用。

鏈式調用

接下來咱們實現普通的鏈式調用,實現鏈式調用很是簡單。因爲then是掛載到this上的方法,若是咱們在then中直接返回this就能夠實現鏈式調用了。就像這樣:

then函數返回的仍是實例對象自己,因此就能夠一直調用 then方法。同時 okCallback應該變成一個數組,才能保存屢次調用 then方法的回調。而當 okCallback是一個數組時,調用resolve方法就須要遍歷 okCallback,依次調用,就像下面這樣:

resolve中,每次調用函數的返回值將會成爲下一個函數的參數。以此就能夠進行 then回調的參數傳遞了。

狀態引入和延時機制

Promise規範裏,最初的狀態是pending,當調用resolve以後轉變爲fulfilled,而調用reject以後轉變爲rejected。狀態只能轉變一次。

另外,爲了保證resolve調用時,then已經所有註冊完畢,還應該引入setTimeout延遲resolve的執行:

鏈式調用Promise(重點)

實際開發中,常常會遇到有先後順序要求的異步請求,咱們每每會在then回調裏返回一個Promise實例,這意味着咱們須要等待這個新的Promise實例resolve以後才能繼續進行下面的then調用。

目前咱們的 MyPromise顯然不能支持這種場景,那麼怎麼才能實現這個執行權的交替呢?

有一個很簡單的方法,還記得then函數的做用嗎?

對,註冊回調

既然它返回了一個新的Promise致使咱們不能正常執行後續的then回調,那咱們直接把後續的then回調所有轉移到這個新的Promise上,讓它代替執行不就行了嗎?

當判斷返回值爲 Promise實例時,直接調用新實例的 then方法註冊剩餘的回調,而後直接 return,等到新實例 resolve時,就會繼續代替執行剩下的 then回調了。

完了嗎?完了,到這裏已經可以實現Promise的鏈式調用了,也就說今天的8分鐘你已經能夠寫出本身的Promise了,恭喜!


不過,這種作法並不是Promise標準,想知道在Promise標準裏是怎麼解決執行權的轉交問題嗎?也不復雜,可是須要你有很是好的耐心去仔細理解裏面的邏輯,準備好了就接着往下看吧~

Promises/A+標準下的鏈式調用

(爲了簡化模型,這裏咱們只分析resolve部分的邏輯,這將涉及到3個函數:thenhandleresolve)

這三個函數的具體做用:

then:此時then函數再也不返回this,而是直接返回一個全新的Promise,這個Promise就是鏈接兩個then之間的橋樑。

handle:當狀態爲pending時,註冊回調。不然直接調用。

resolve:嘗試遍歷執行註冊的回調。若是參數是一個promise實例,則將執行權移交給新的promise,自身暫停執行。

看到這裏是否是以爲很複雜?其實也不復雜,一句話就能夠歸納:

在promises/A+規範裏,後一個promise保存了前一個promise 的resolve引用。

前一個 resolve會帶動後一個 resolve,當 resolve的參數是 Promise實例時,暫停自身 resolve的調用,把自身做爲引用傳遞給新的 Promise實例,新的 Promise實例的 resolve會引發自身 resolve

若是還不理解的話,咱們能夠實際看一個例子。(請一邊看例子,一邊對照着代碼思考哦,代碼在最後附錄,建議粘貼到本地編輯器對照着思考。)

比基尼海灘的海綿寶寶想要外賣一個蟹黃堡,他必須首先上網查到蟹堡王的外賣電話,而後才能點外賣。用JavaScript描述就是下面這樣:

在最開始的階段,一共會生成3個 promise,分別是 getPhoneNumber(),第一個 then()和第二個 then()( getHamburger還未調用,所以沒有計算在內)

讓咱們來看看對應的代碼執行吧。如今是註冊回調階段,第一個then返回的promise將會把自身的resolve引用傳遞給getPhoneNumber()handle函數,而handle函數會同時把resolve應用和對應的then回調一同保存起來:

第二個then同理,因此註冊階段結束後,各個promise內部的狀態以下圖所示:

在調用階段,會隨着getPhoneNumber()resolve引起後續的resolve,整個過程能夠用下圖表示:

  1. 首先,getPhoneNumber()觸發resolve,返回值是number,所以能夠遍歷調用callbacks裏保存的回調。
  2. 進入handle函數,改變getPhoneNumber()state,以後函數①被調用,該函數返回getHamburger()。調用第一個thenresolve引用,並將getHamburger()做爲參數傳遞。
  3. 視線轉移到第一個then()。在判斷參數getHamburder()是一個Promise實例以後,將自身的resolve做爲回調,調用其then方法(能夠看到上圖中,getHamburgercallbacks裏保存的實際上是前一個thenresolve引用,由於此時前面的Promise被中斷了,所以當開發者調用getHamburger()resolve方法時,才能繼續未完成的resolve執行。)
  4. 等到getHamburger()resolve調用時,實際上就會調用上一個thenresolve,返回值做爲參數傳遞給右邊的then,使其resolve
  5. 視線再一次到第一個then()上。進入自身的handle方法,改變state,以後函數②被調用,觸發第二個then()(也就是上圖最右邊)的resolve
  6. 最後,調用最後一個then()resolve(也就是上圖中的小then(),這個then()是代碼自動調用生成的。),整個異步過程結束。

總結

Promise使用一個resolve函數讓咱們不用面臨回調地獄(由於回調已經經過鏈式的方式註冊保存起來了),實際上作的就是一層封裝。其中最難理解的部分就是Promise的鏈式調用。本次跟你們分享的第一種方式很是簡單粗暴,即把未執行完的回調轉交給下一個Promise便可。第二種方式本着不拋棄不放棄的原則,多個then函數經過resolve引用連成一氣,前面的resolve將可能會引發後面一系列的resolve,很有多米諾骨牌的感受。

附錄

Promises/A+ 規範代碼示例(來源:參考[1])

function MyPromise(fn) {
  var state = 'pending',
      value = null,
      callbacks = [];

  this.then = function (onFulfilled) {
      return new MyPromise(function (resolve) {
          handle({
              onFulfilled: onFulfilled || null,
              resolve: resolve
          })
      })
  }

  let handle = (callback) => {
      if (state === 'pending') {
          callbacks.push(callback)
          return
      }
      //若是then中沒有傳遞任何東西
      if(!callback.onFulfilled) {
          callback.resolve(value)
          return
      }

      var ret = callback.onFulfilled(value)
      callback.resolve(ret)
  }

  
  let resolve = (newValue) => {
      if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
          var then = newValue.then
          if (typeof then === 'function') {
              then.call(newValue, resolve)
              return
          }
      }
      state = 'fulfilled'
      value = newValue
      setTimeout(function () {
          callbacks.forEach(function (callback) {
              handle(callback)
          })
      }, 0)
  }

  fn(resolve)
}
複製代碼

參考

  1. 30分鐘,讓你完全明白Promise原理 mengera88 2017-05-19
  2. 深刻淺出Nodejs 樸靈 P90
相關文章
相關標籤/搜索