Promise實現的基本原理(一)

Britta發表於2019-04-14

前言

前幾天在專案中遇到回撥地獄的情況,但我在剛開始寫這個專案的時候還不會用Promise,只知道它可以用來解決回撥地獄,所以就用全都用回撥函式解決。我是看《深入理解ES6》這本書來學Promise的,在用法和功能上講得還是挺詳細的,但在原理上沒怎麼講,所以用的時候有很多地方有疑問,於是在網上找到一篇自己實現Promise的文章來理解Promise實現原理。

參考連結:www.mattgreer.org/articles/pr…

一、最簡單的用法

new Promise( (resolve, reject) => {
    resolve(42)
}).then( result => {
    console.log(result)
})

// 控制檯輸出
// 42
複製程式碼

這是一段最簡單的Promise使用(我們先不討論拒絕reject,假設所有的操作都被接受resolve),很開心,終於會用Promise了(心虛~),但我在使用的時候出現了一些疑問,在我說我的疑問之前,我們先說一下函式的回撥

回撥函式

在js中,函式a作為引數傳給另一個函式b,此時函式a就叫做回撥函式。在函式b的函式體中呼叫傳進來的函式a,此時函式a就被函式b回撥了。

OK,現在我們懂什麼是回撥函式了,我可以說我的疑問了

二、我的疑問 —— resolvethen

首先我們先分析一下上面最簡單的例子中有多少個回撥函式,我把上面的程式碼重新寫一下:

// 現在不用箭頭函式,用變數直觀點,這裡就不寫reject了
let fn1 = function (resolve) {
    resolve(42)
}

let fn2 = function (result) {
    console.log(result)
}

new Promise(fn1).then(fn2)  // 42
複製程式碼

new Promise(fn1).then(fn2) 這行程式碼可以很直觀地看出這裡有 2 個回撥函式,但還沒完,再看看fn1這個函式

let fn1 = function (resolve) {
    // resolve再這裡被呼叫了,說明resolve是個回撥函式
    resolve(42)
}
複製程式碼

所以現在我們的會調函式是這樣的:

new Promise的時候給Promise傳入了一個含有resolve這個回撥函式的回撥函式fn1,建立完Promise物件之後,隨即呼叫了Promise物件中的then()方法,給then方法傳入了一個回撥函式fn2。(這裡涉及到一點點的new操作符的作用)

好了,分析完回撥函式之後我開始想不通了:

  • 疑問1:什麼時候呼叫fn1我都不知道
  • 疑問2:我沒有給fn1傳回撥函式啊
  • 疑問3:就先裝作我知道fn1是什麼時候呼叫的吧,那為什麼在fn1呼叫resolve的時候,為什麼看起來像呼叫的是我傳過去給then的回撥函式fn2
  • 啊?

三、實現原理

終於回到正題了,解決一下我上面的疑問。

在開始的那篇參考的文章中,作者實現了一個還算比較完整的Promise,內容有點長,我就只擷取其中能夠實現上面寫的最簡單Promise使用的原理。

function Promise(fn) {
  var state = 'pending';        // Promise當前的狀態
  var value;                    // 當Promise處於接受狀態時儲存下來的值,這是結果
  var deferred;                 // 延期執行函式(現在還看不出來它是函式)

  function resolve(newValue) {  // Promise被接受之後執行的函式
    value = newValue;           // 儲存獲取到的結果
    state = 'resolved';         // 將狀態改為resolved

    if(deferred) {              // 如果deffred不是undefined,已經有指向函式的引用
      handle(deferred);         // 將deffred傳給handle函式處理
    }
  }

  /*
   * 當使用者呼叫then函式時呼叫的處理函式
   * 等等就解釋
   */
  function handle(onResolved) {
    if(state === 'pending') {   // 判斷當前所處的狀態,如果是padding狀態,即沒有被處理狀態,還處於初始狀態
      deferred = onResolved;    // 將onResolved回撥函式儲存下來,稍後執行
      return;                   // padding狀態就不執行下面的語句
    }

    onResolved(value);          // 如果不是padding狀態,呼叫onResolved,並把結果傳給回撥函式
  }

  /*
   * 這個是我的疑問then了
   * 這裡涉及一個關鍵字this,涉及的內容有點多,先不說
   * 看等號後面是一個function,所以它是一個函式,並且傳了一個引數,是回撥函式
   * 從上面的的程式碼可以看出onResolved是一個回撥函式
   */
  this.then = function(onResolved) {
    handle(onResolved);         // 每當呼叫then這個函式,就將onResolved(例子中的fn2)傳給handle函式
  };
  
  /*
   * 上面全都是函式和變數的定義,只有這一行是執行函式
   * 疑問1解決:在new Promise的時候就執行了傳過來的回撥函式fn1
   * 疑問2解決:在呼叫fn1的時候傳過去的回撥函式是Promise的內部函式resolve
   * 疑問3解決:有點繞,我寫在程式碼外面
   */
  fn(resolve);
}
複製程式碼

疑問三解決:

前兩個疑問的解決請看fn的註釋

我們在用Promise物件呼叫then函式的時候給他傳了fn2,也就是onResolved是指向fn2的,然後onResolved又傳給了handle,在handle裡面呼叫了onResolved,也就是說,fn2handle裡呼叫。

現在我們知道了fn2是在handle裡被呼叫了。然後handle在哪裡被呼叫

因為例子中沒有非同步操作,所以先不考慮padding情況:

handle函式在then函式中呼叫

再看回最簡單的Promise用法:

let fn1 = function (resolve) {
    resolve(42)
}

let fn2 = function (result) {
    console.log(result)
}

new Promise(fn1).then(fn2)  // 42
複製程式碼

1. 執行new Promise(fn1)時做了什麼?

當程式執行到new Promise時,Promise裡面的語句fn(resolve)會馬上執行,呼叫了我們傳過去的回撥函式fn1,並且給我們傳回一個值叫resolve,這個值恰好又是一個回撥函式(引數名resolve的指向是Promise物件中的resolve()函式,我們在這個例子中沒有寫任何的非同步操作,所以東西幾乎都是同步執行的)。我們又馬上呼叫了resolve回撥函式,並給它傳入一個值42(newValue),state被置為'resolved',此時new Promise就全部執行完成。總結一下上面這段話:我們這個例子沒有非同步操作,所以resolve函式在new Promise的時候就被呼叫了完了,狀態改成了'resolved'

2. 執行then(fn2)時做了什麼?

例子中建立完Promise物件之後馬上呼叫了then函式,並且傳入了一個會調函式fn2(這裡的then其實是一個微任務,這裡先不做討論,暫且當它是馬上執行的)。呼叫then時,then呼叫了Promise物件的函式handle並且把onResolved指向了fn2,因為在new Promise的時候就已經把status改成'resolved'了,所以handle函式體裡面的if語句不成立,於是就執行onResolved(value)並且把new Promise中的到的值傳入,也就是說fn2handle回撥了。總結上面的這段話:在我們呼叫then的時候呼叫了handle,並且把new Promise時得到的值傳回給fn2回撥函式

到現在疑問三解決了。

總結疑問三:

fn被呼叫的時候,給fn1傳回了Promise的內部函式resolve,被fn1的resolve引數所接收,所以在fn1方法體內呼叫的resolve實際是指向Promise的內部函式resolve,並未我們給他傳了42這個值,被Promiseresolve函式儲存在value當中。在呼叫then函式的時候,value被handle傳出來給fn2

終於解決完問題了!!

關於Promise的三種狀態的作用,還有如何實現序列的Promise,和reject,看完那篇參考文章的還是很好理解的,裡面還有很多東西,我就不寫了,去看吧。

四、Promise 的使用和理解中涉及到的知識點

  • new 關鍵字建立物件
  • this關鍵字
  • 原型鏈
  • js和瀏覽器的非同步機制
  • event loop

相關文章