來了老弟,最簡單的Promise原理

阿海Vijay發表於2019-04-03

引言

Promise是一種非同步程式設計的解決方案,通過鏈式呼叫的方式解決回撥地獄。作為前端面試中的考點,也是前端的基本功,掌握其原理是非常重要的。本次分享就從Promise的使用方式上出發,一步一步剖析其原理,最後幫助大家封裝出自己的Promise

注:如果你還不瞭解Promise,建議點選這裡學習Promise的基本使用語法。

本文全長2665個字,閱讀8分鐘你將收穫:

  • Promise的原理
  • 構建自己的Promise函式的能力

閱讀60分鐘你將徹底明白Promise的原理。

正文


知其然才能知其所以然,我們先來看一下最常使用的例子,分析有什麼特徵。

使用例子

來了老弟,最簡單的Promise原理
熟悉Promise使用的人都知道,當呼叫getNews()返回一個新的Promise時,裡面的非同步呼叫操作將會立刻執行,然後在非同步回撥裡呼叫resolve方法和reject方法改變Promise的狀態,執行對應的then或者catch函式。

以上程式碼有以下幾個特徵:

  1. Promise是一個建構函式,其接受一個函式作為引數。
  2. 作為引數的函式裡,有兩個方法resolvereject
  3. Promise帶有方法thencatch

resolvereject是怎麼來的?Promise例項化的時候都做了什麼?

別急,所謂生死看淡,不服就幹。在回答這兩個問題之前,我們可以先直接嘗試構建自己的Promise

開始構建

  1. 建構函式

來了老弟,最簡單的Promise原理
fn就是我們使用時傳入的回撥函式。

  1. resolvereject

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

來了老弟,最簡單的Promise原理
至此,第一個問題得到了回答,即:resolverejectPromise內部提供給開發者的。

  1. 新增thencatch方法

這兩個方法是Promise例項的方法,因此應該寫在this或者prototype上。

來了老弟,最簡單的Promise原理
到這裡,一個Promise基本的骨架就出來了,下面我們仔細嘮嘮這4個函式的具體作用。

作用分析

  1. resolvereject

想象一下我們日常使用Promise的場景,在非同步請求之後,是需要我們手動呼叫resolvereject方法去改變狀態的。

來了老弟,最簡單的Promise原理
resolve呼叫意味著非同步請求已經有了結果,可以執行then裡面的回撥了(reject同理,非同步請求失敗時候執行catch函式。)

  1. thencatch

呼叫上述的函式時如下:

來了老弟,最簡單的Promise原理

我們知道,在resolve被呼叫前,thencatch函式裡面的回撥是不會執行的。那麼我們這樣寫的時候,它做了什麼呢?

實際上,Promise悄悄把我們寫的回撥函式儲存了起來,等到我們手動呼叫resolve或者reject時才依次去執行。也就是說,Promise裡的thencatch的作用就是:註冊回撥函式,先把一系列的回撥函式存起來,等到開發者呼叫的時候才拿出來執行。

所以,thencatch 函式應該長這樣:

來了老弟,最簡單的Promise原理
resolvereject就是分別去呼叫他們而已。

來了老弟,最簡單的Promise原理
到目前為止可以回答第二個問題了:Promise初始化時,內部呼叫了我們傳入的函式,並將resolvereject方法作為引數提供。在之後呼叫的then或者catch方法裡,把回撥函式儲存在內部的一個佇列中,等待狀態改變時候呼叫。

鏈式呼叫

接下來我們實現普通的鏈式呼叫,實現鏈式呼叫非常簡單。由於then是掛載到this上的方法,如果我們在then中直接返回this就可以實現鏈式呼叫了。就像這樣:

來了老弟,最簡單的Promise原理
then函式返回的還是例項物件本身,所以就可以一直呼叫then方法。同時okCallback應該變成一個陣列,才能儲存多次呼叫then方法的回撥。而當okCallback是一個陣列時,呼叫resolve方法就需要遍歷okCallback,依次呼叫,就像下面這樣:

來了老弟,最簡單的Promise原理
resolve中,每次呼叫函式的返回值將會成為下一個函式的引數。以此就可以進行then回撥的引數傳遞了。

狀態引入和延時機制

Promise規範裡,最初的狀態是pending,當呼叫resolve之後轉變為fulfilled,而呼叫reject之後轉變為rejected。狀態只能轉變一次。

另外,為了保證resolve呼叫時,then已經全部註冊完畢,還應該引入setTimeout延遲resolve的執行:

來了老弟,最簡單的Promise原理

鏈式呼叫Promise(重點)

實際開發中,經常會遇到有前後順序要求的非同步請求,我們往往會在then回撥裡返回一個Promise例項,這意味著我們需要等待這個新的Promise例項resolve之後才能繼續進行下面的then呼叫。

來了老弟,最簡單的Promise原理
目前我們的MyPromise顯然不能支援這種場景,那麼怎麼才能實現這個執行權的交替呢?

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

對,註冊回撥

既然它返回了一個新的Promise導致我們不能正常執行後續的then回撥,那我們直接把後續的then回撥全部轉移到這個新的Promise上,讓它代替執行不就好了嗎?

來了老弟,最簡單的Promise原理
當判斷返回值為Promise例項時,直接呼叫新例項的then方法註冊剩餘的回撥,然後直接return,等到新例項resolve時,就會繼續代替執行剩下的then回撥了。

完了嗎?完了,到這裡已經能夠實現Promise的鏈式呼叫了,也就說今天的8分鐘你已經可以寫出自己的Promise了,恭喜!


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

Promises/A+標準下的鏈式呼叫

(為了簡化模型,這裡我們只分析resolve部分的邏輯,這將涉及到3個函式:thenhandleresolve)

來了老弟,最簡單的Promise原理
這三個函式的具體作用:

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

handle:當狀態為pending時,註冊回撥。否則直接呼叫。

resolve:嘗試遍歷執行註冊的回撥。如果引數是一個promise例項,則將執行權移交給新的promise,自身暫停執行。

看到這裡是不是覺得很複雜?其實也不復雜,一句話就可以概括:

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

來了老弟,最簡單的Promise原理
前一個resolve會帶動後一個resolve,當resolve的引數是Promise例項時,暫停自身resolve的呼叫,把自身作為引用傳遞給新的Promise例項,新的Promise例項的resolve會引起自身resolve

如果還不理解的話,我們可以實際看一個例子。(請一邊看例子,一邊對照著程式碼思考哦,程式碼在最後附錄,建議貼上到本地編輯器對照著思考。)

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

來了老弟,最簡單的Promise原理
在最開始的階段,一共會生成3個promise,分別是getPhoneNumber(),第一個then()和第二個then()(getHamburger還未呼叫,因此沒有計算在內)

來了老弟,最簡單的Promise原理

讓我們來看看對應的程式碼執行吧。現在是註冊回撥階段,第一個then返回的promise將會把自身的resolve引用傳遞給getPhoneNumber()handle函式,而handle函式會同時把resolve應用和對應的then回撥一同儲存起來:

來了老弟,最簡單的Promise原理

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

來了老弟,最簡單的Promise原理

在呼叫階段,會隨著getPhoneNumber()resolve引發後續的resolve,整個過程可以用下圖表示:

來了老弟,最簡單的Promise原理

  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

相關文章