Promise實現的基本原理(二)

Britta發表於2019-05-02

前言

距離我上次寫的那篇Promise實現的基本原理(一)有十幾天了(太懶了),這篇我想寫Promise是如何實現串聯呼叫then,解決回撥地獄的。雖然說《深入理解ES6》這本書把Promise用法講得很詳細,但是自己理解起來還是有很多疑問(我好想上一篇也是這麼說的)。

參考連結:JavaScript Promises ... In Wicked Detail

github上的完整實現:Bare bones Promises/A+ implementation

一、Promise可能會出現的非同步情況

情況一:對resolve的呼叫是非同步的

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('resolve')
    console.log('Promise setTimeout')
  }, 1000)
}).then(result => {
  // 這個箭頭函式我們叫 fn1
  console.log(result)
}, error => {
  // 這個箭頭函式我們叫 fn2
  console.log(error)
})
複製程式碼

情況二:對then的呼叫是非同步的

let p1 = new Promise((resolve, reject) => {
  resolve('resolve')
  console.log('Promise setTimeout')
})

setTimeout(() => {
  p1.then(result => {
    // 這個箭頭函式我們叫 fn1
    console.log(result)
  }, error => {
    // 這個箭頭函式我們叫 fn2
    console.log(error)
  })
}, 1000)
複製程式碼

情況三:呼叫resolve和呼叫then都是非同步的

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('resolve')
    console.log('Promise setTimeout')
  }, 1000)
})

setTimeout(() => {
  p2.then(result => {
    // 這個箭頭函式我們叫 fn1
    console.log(result)
  }, error => {
    // 這個箭頭函式我們叫 fn2
    console.log(error)
  })
}, 1000)
複製程式碼

第三種情況是前兩種情況的結合,如果第一個setTimeout時間比第二個setTimeout時間長,那就是第一種情況;如果第二個setTimeout比第一個setTimeout的時間長,那就是第二種情況。瞭解event-loop的同學看著幾種情況應該比較清晰,不瞭解的同學就看我下面的分析。

二、Promise三種狀態是如何工作的

我從《深入理解ES6》Promise那章擷取了一下內容:

每個Promise都會經歷一個短暫的生命週期:先是處於進行中(padding)的狀態,此時操作尚未完成,所以他也是未處理(unsettled)的;一旦非同步操作執行結束,Promise則變為已處理(settled)的狀態,操作結束後Promise可能會進入到以下兩個狀態的其中一個:

  • Fulfilled Promise 非同步操作成功完成
  • Rejected 由於程式錯誤或者一些其他原因,Promise 非同步操作未能成功完成

Promise的內部屬性[[PromiseState]]被用來表示Promise的3中狀態:"pedding""fulfilled""rejected"

實現程式碼:

function Promise(fn) {
  var state = 'pending';            // 初始狀態為padding
  var value;                        // Promise處於被處理狀態下儲存下來的值
  var deferred = null;              // 延期執行函式(現在還看不出來它是函式)

  /**
   * 非同步操作成功完成回撥函式
   * 在new Promise的時候被呼叫的(不清楚為什麼的可以看上一篇部落格)
   */
  function resolve(newValue) {   
    // 如果在then的回撥函式裡面返回了一個Promise
    if(newValue && typeof newValue.then === 'function') {
      // 將這個Promise的then的兩個回撥引數的引用指向Promise的resolve和reject
      newValue.then(resolve, reject);   
      return;
    }
    
    // 如果傳進來的不是Promise
    state = 'resolved';             // 將Promise的狀態改成resolved(fulfilled)
    value = newValue;               // 儲存return過來的值

    if(deferred) {                  // 判斷是否存在傳進來的回撥函式
      handle(deferred);             // 如果有,將我們傳進來的回撥函式交給handle
    }                               //(不清楚為什麼的可以看上一篇部落格)
  }

  /**
   * 非同步操作失敗/被拒絕回撥函式
   * 在 new Promise 的時候被呼叫的
   */
  function reject(reason) {         
    state = 'rejected';             // 將Promise的狀態改成rejected
    value = reason;                 // 儲存我們傳進來的被拒絕原因

    if(deferred) {                  // 和 resolve同理
      handle(deferred);             
    }
  }

  function handle(handler) {
    // handle被呼叫了,但是此時promise的狀態還是處於出事的未處理狀態
    if(state === 'pending') {
      deferred = handler;           // 就將handler的引用先儲存下來
      return;                       // 不執行下面的程式碼
    }
    
    var handlerCallback;            // Promise處理完成的狀態只能是一種(fulfilled/rejected),所以用同一個變數儲存回撥的引用

    if(state === 'resolved') {      // 如果已經成功處理(fulfilled)
      handlerCallback = handler.onResolved;     // 引用是處理成功回撥的引用
    } else {
      handlerCallback = handler.onRejected;     // 如果不是,引用是處理失敗回撥的引用
    }

    if(!handlerCallback) {          // 如果在呼叫then的時候沒有給then傳回撥函式,即上一個if語句儲存下來的引用是undefined的
      if(state === 'resolved') {    // 如果狀態已經是resolved(fulfilled)的
        handler.resolve(value);     // 就呼叫hander的resolved(下一個then傳入的成功回撥函式)
      } else {
        handler.reject(value);      // 否則,呼叫handler的reject(下一個then傳入的第二個引數,即失敗回撥函式)
      }
      return;                       // 不執行下面的語句
    }
    
    // 如果本次呼叫的then有傳入回撥函式
    var ret = handlerCallback(value);   
    handler.resolve(ret);           // 就給
  }

  this.then = function(onResolved, onRejected) {
    return new Promise(function fn3 (resolve, reject) {  // then 返回一Promise
      // 返回的Promise把then傳進來的兩個回撥和本省的兩個回撥都丟給handle處理
      handle({                                      
        onResolved: onResolved,
        onRejected: onRejected,
        resolve: resolve,
        reject: reject
      });
    });
  };

  fn(resolve, reject);
}
複製程式碼

回撥函式引用分析:

fn函式的引用在上一篇已經分析過了,這一片重點分析在then中返回Promise給handle函式傳過去的物件屬性引用

  • onResolved: 在我們用Promise物件呼叫then的時候傳過來的第一個引數 fn1 (我在註釋裡給了個名字好區分,就不像第一篇那樣一個一個拆出來了,有點懶)。
  • onReject: 在我們用Promise物件呼叫then的時候傳過來的第二個引數fn2。
  • resolve: then返回的Promise物件傳過去的fn3, 當fn3被呼叫時Promise傳給它的第一個引數就是resolve的引用,所以resolve (這是key) 的引用是Promise的內部函式resolve (這是function)。當然,每一次new Promise都會是一個新的物件,所以內部的resolve的引用也是不一樣的。
  • rejectrejectresolve同理,指向Promise內部函式reject

handle函式分析

1、handle引數分析

handle函式的呼叫還是在使用者呼叫then的時候呼叫,只不過這次then函式返回了一個Promise,給handle函式傳入的值既保留了本次呼叫then傳入的兩個回撥函式的引用,也保留了新的Promise的回撥函式引用。

2、結合三種狀態分析

   (1)"pending"狀態:

    當呼叫handle函式時Promise處於padding對應的情況是情況一,此時使用者建立了Promise物件後立即呼叫了then函式,但resolve還沒有被處理,所以Promise狀態沒有改變,then的回撥函式需要等待resolve被處理了才可以執行。所以當padding狀態的時候,將handler這個引數交給deferred暫存。

   (2)"resolved"/rejected狀態:

    一秒後,resolve回撥函式被呼叫了(關於resolve的指向可以看第一篇),Promisestate被置為"resolved"了,然後只要deferred有指向,就再一次交給handle函式處理一次。

    此時Promise的狀態是"resolved",將handlerCallback的引用指向fn1,又將handleCallback返回的值儲存在了ret中,同時又傳給then返回的Promiseresolve,讓返回的Promise處於已處理狀態(只有當resolve被執行了,then的回撥函式才會執行,才能夠實現then的無限串聯呼叫),也就是一下這種情況:

let p = new Promise((resolve, reject) => {
  resolve(42)
})

p.then().then().then(result => {
    console.log(result)   // 控制檯輸出42
})
複製程式碼

三、如何實現then的串聯使用

上面那一點其實就可以看出,Promise是通過在then函式裡面返回了一個Promise,實現了Promisethen的串聯使用。但上面的那情況並沒有真正體現Promise的作用,請看以下程式碼:

new Promise((resolve, result) => {  // Promise1
  resolve(1)
}).then(result => { // Promise1的then
  console.log(result)   // 1
  return new Promise((resolve, reject) => { // Promise3
    resolve(2)
  })
}) // 此處得到Promise2
.then(result => { // Promise2的then
  console.log(result)   // 2
  return 3
}) // 此處得到Promise4
.then(result => { // Promise4的then
  console.log(result)   // 3
}) // 此處得到Promise5
複製程式碼

在實現程式碼裡面可以看到then時返回了一個新的Promise的,然後再看上面的程式碼,在呼叫then的第一個回撥函式回撥的時候,我們給他返回了一個引數,一種是Promise物件,一種是返回一個數值,到這裡我的疑問是then內部返回的Promise是如何拿到這些值的,以下分析?上面的程式碼

Promise個數分析

上面那段程式碼裡裡外外有5個Promise,先起個小名,分別是:

  • new的時候有一個 —— Promise1
  • Promise1的then內部返回了一個 —— Promise2
  • Promise1的then的第一個回撥函式返回了一個 —— Promise3
  • Promise2的then內部返回了一個 —— Promise4
  • Promise4的then內部返回了一個 —— Promise5

1、Promise1:

handler引數四個屬性指向:

  • onResolved: Promise1的then的第一個引數(回撥函式)
  • onRejected: Promise1的then的第二個引數(回撥函式)
  • resolve: Promise2的內部函式resolve
  • reject: Promise2的內部函式reject

Promise1當前的狀態是"resolved"

then被呼叫,所以handle被呼叫blablabla~

handleCallback指向handler.onResolved

各種判斷賦值後到了handle的最後兩行,此時ret接收到handleCallback返回的值,然後這個值是Promise3。接著呼叫了handle.resolve,並且給它傳了ret,而handle.resolve指向的是Promise2的resolve,所以接下來就跳到Promise2了。

2、Promise2:

handler引數四個屬性指向:

  • onResolved: Promise2的內部函式resolve
  • onRejected: Promise2的內部函式reject
  • resolve: Promise4的內部函式resolve
  • reject: Promise4的內部函式reject

Promise2的resolve在Promise1的handle函式的最後一條語句被呼叫,並且傳入的ret是一個Promise(Promise3)

所以Promise2的resolve函式中的第一個語句成立,但Promise2的狀態不改變,newValue指向的是Promise3,需要等待Promise3被處理。

接著Promise3的then被呼叫,所以接下要調到Promise3了。

3、Promise3

handler引數四個屬性指向:

  • onResolved: Promise2的內部函式resolve
  • onRejected: Promise2的內部函式reject
  • resolve: Promise3的then返回的Promise的內部函式resolve(在當前例子不是很重要,忽略)
  • reject: Promise3的then返回的Promise的內部函式reject(在當前例子不是很重要,忽略)

在Promise2呼叫Promise3的then的時候,Promise3的狀態未知,但不管怎樣,只要Promise的狀態改變了,就會呼叫一次handle函式。

某個時刻Promise3的狀態變為了"resolved"/"rejected"或者在Promise2呼叫它的時候就已經是"resolved"/"rejected"狀態

還是到handle函式的最後兩行,和Promise1的一樣,handler.onResolved會被執行,並且把Promise3的resolve回撥時得到的值value傳給Promise2的內部函式resolve,Promise2變為了"resolved"狀態,並且的到了Promise3被處理後的value

Promise3此時的任務已經完成了,被處理了,並且把得到的value傳給了Promise2,拜拜?。

4、回到Promise2

handler引數四個屬性的指向還是和之前一樣,在Promise3被處理之後,Promise2的狀態也會跟著Promise3的狀態變化,並且拿到了Promise3的value,所以先在Promise2的value是2

Promise2狀態變為已處理之後,不管什麼時候呼叫thenthen的兩個回撥函式其中一個會被回撥,所以Promise的這種處理,會讓我這種菜?覺得then的回撥函式裡面returnPromise是變成了thenreturn,但其實Promise做的事情只是儲存值而已。

5、Promise4

Promise4的handler引數屬性指向其實和Promise2是差不多的,和Promise2不同的是:ret的到不是一個Promise,而是一個具體的值。也就是說newValue = 3,newValue不是Promise就好辦了,把狀態改成"resolved",儲存一下return過來的值,其他和最開始的Promise處理方式一樣,完事。

總結

Promise能夠串聯呼叫then,還是儲存值和各種引用的轉換,Promise大法好!

相關文章