ES6 Promise 全面總結

ES6 Promise對象

ES6中,新增了Promise對象,它主要用於處理異步回調代碼,讓代碼不至於陷入回調嵌套的死路中。javascript

@-v-@html


1. Promise本質

Promise本質上是一個 函數 ,更確切地說,它是一個 構造器 ,專門用來構造對象的。
它接受一個函數做爲參數,並返回一個對象,大體狀況以下:java

function Promise( fn ){
    // var this = {}
    // Object.setPrototypeOf(this, Promise.prototype)
    // 接下來,是Promise函數具體要實現的功能,這部分由系統幫咱們完成
    ...  
    // 最後返回這個Promise實例
    return this
  }

Promise函數的參數,做爲函數形式存在,須要咱們手動去編寫。
它須要兩個參數,狀況以下:git

function fn(resolve, reject){
    ...  // 咱們本身寫的邏輯代碼
  }

Promise函數的返回值是一個對象,準確來講,是Promise本身生成的實例。
其實Promise函數的使命,就是構建出它的實例,而且負責幫咱們管理這些實例。
該實例有三種狀態,分別是: 進行 狀態、 完成 狀態 和 失敗 狀態。
該實例只能從「 進行 狀態」轉變爲「 完成 狀態」,或從「 進行 狀態」轉變爲「 失敗 狀態」,這個過程不可逆轉,也不可能存在其餘可能。由於Promise就是用來管理業務狀態的一種機制,它可以保證業務的順序執行,而不出現混亂。 es6

這就比如咱們在家裏炒一份菜,是隻可能存在「 正在炒菜 」、「 炒好了 」和「 炒糊了 」這三個階段的,而「正在炒菜」的狀態確定是會優先存在於「炒好了」和「炒糊了」兩個狀態前面,「炒好了」和「炒糊了」自己又是兩個 互斥的事件 ,因此這個過程,只可能出現從「正在炒菜」狀態過渡到「炒好了」或者「炒糊了」狀態的狀況,永遠不可能從「炒好了」過渡到「炒糊了」狀態,也不可能從「炒糊了」過渡到「炒好了」狀態。 github

那麼,這些由Promise函數構建出來的對象,究竟有着什麼用處呢?
咱們先來看一組代碼:編程

fn( ( ( ( ()=>{} )=>{} )=>{} )=>{} )

像這樣回調之中調回調的狀況,在Node開發中,是一件很常見的事。
Node自己是一個無阻塞、無空耗、併發、依賴於系統底層讀寫事件的運行環境,它的回調機制保證了它在異步併發執行過程當中回調鏈的獨立性和抗干擾能力,但同時也帶來了很大的反作用,最大的麻煩就是,採用普通回調方式書寫出來的Node回調代碼十分混亂。 數組

其實,面向過程或面向對象的函數式編程,自己就是一個巨大的「函數調用」過程。咱們在代碼中使用函數,並在函數中調用函數,運行環境幫助咱們維護一個或多個函數棧,以實現程序的有序執行,及加強軟件後期維護的便利性。 promise

但若是咱們能把這種不斷調用的過程給攤開成 平面 ,而不要使函數相互嵌套,就會使咱們的軟件可維護性提高很大一個臺階。咱們只須要將本來寫好的功能一個個羅列出來,並構造出一根供函數調用的鏈條,把這些功能一個個地按需調用,軟件的功能不就實現了麼?並且還更清晰明瞭。
Promise幫助咱們將函數攤開來,造成一根調用鏈條,讓程序有序執行。 bash

每個返回值爲Promise實例的函數,都是 Promise調用鏈條上的一個結點 ,這個Promise實例維護着該處函數的運行狀態,並決定着自身的生存週期。它的寫法大體是這樣的:

// 執行一個返回值爲promise的函數 並經過resolve或reject返回
  promiseFn_1(...)
  // 將多個返回值爲promise的函數合成一個 並經過resolve或reject返回
  // Promise.all( promiseFn_all_1, promiseFn_all_2, ... )
  // Promise.race( promiseFn_race_1, promiseFn_race_2, ... )
  //
  .then(
    (...resolveArgs)=>{ ... promiseFn_resolve_1(...) ... },
    (...rejectArgs)=>{ ... promiseFn_reject_1(...) ... },
  )
  .then(
    (...resolveArgs)=>{ ... promiseFn3_resolve_2(...) ... },
    (...rejectArgs)=>{ ... promiseFn3_reject_2(...) ... },
  )
  ...
  .catch(
    (...rejectArgs)=>{ ... promiseFn_catch_1(...) ... }
  )
  ...
  .finally(
    (...simpleArgs)=>{ ... }
  )

上面的代碼看似及其繁瑣,其實結構層次已經比使用普通回調方式書寫的代碼好不少了(雖然仍是顯得有些混亂)。
當咱們瞭解了Promise中這些函數(如then()、catch()、finally())的具體意思,就會明白它的具體意思了。
接下來咱們就來構建一個Promise實例,看一看這根「鏈條」上的結點(也就是上面以「promiseFn_」開頭的函數)到底長什麼樣。

function promiseFn_1(path, options){
    return new Promise((resolve,reject)=>{
      // 須要執行的具體代碼,通常狀況下,是調用一個帶有回調參數的函數
      // 此處使用fs模塊中的readFile函數做爲示例
      fs.readFile(path, options, (err,data)=>{
        if(err){
          reject(err)
          // 這樣使用可能會更好:
          // throw new Error(path+' :  文件讀取出現未知的錯誤!')
        }
        resolve(data)
      })
    })
  }

上面Promise參數函數中,出現了兩個陌生的參數,resolve和reject。它們實際上是在Promise運行完成後,主動向該回調函數中傳入的參數。這個過程,由Promise函數自動幫咱們完成。
resolve和reject都是與Promise實例相關的函數,用於改變Promise實例的狀態。
resolve函數能使Promise實例從「進行」狀態變成「完成」狀態,並將本身接受到的參數傳給下一個promise對象。
reject函數能使Promise實例從「進行」狀態變成「失敗」狀態,並將本身接受到的參數傳給下一個promise對象(通常是一個錯誤對象)。

2. Promise的幾個重要方法

2.1 promise Promise.prototype.then( resolveFn, [rejectFn] )

@param resolveFn( ...args )  
    函數,當Promise實例狀態變爲「完成」狀態時會被執行,  
    用於將從當前promise中取出reresolve( ...args )中獲得的參數(...args),  
    並進行相應的操做,好比將(args)傳入另外一個封裝了promise構造器的函數,  
    並將該函數執行完成後返回的promise實例返回  
    @param ...args  
      參數列表,當前promise實例處於「完成」狀態時,經過resolve(...args)獲得的值。  
  @param [rejectFn( ...args )]  
    函數,可選,當Promise實例狀態變爲「失敗」狀態時會被執行,  
    用於將從當前promise中取出reject( ...args )中獲得的參數(...args),  
    並進行相應的操做,好比將(args)傳入另外一個封裝了promise構造器的函數,  
    並將該函數執行完成後返回的promise實例返回  
    @param ...args  
      參數列表,當前promise處於「完成」狀態時,經過resolve(...args)獲得的值。  
  @return promise  
    promise對象,resolveFn或rejectFn執行後的返回值,  
    咱們通常會在fn中調用另外一個封裝了promise構造器的函數,  
    而後將其返回給then()方法,then()方法再將其做爲then的返回值返回給當前鏈式調用處,  
    若是fn()返回的不是一個promise對象,then()會幫咱們將fn()返回值封裝成promise對象,  
    這樣,咱們就能夠確保可以鏈式調用then()方法,並取得當前promise中得到的函數運行結果。

then()方法定義在Promise.prototype上,用於爲Promise實例添加狀態更改時的回調函數,至關於監聽同樣。
噹噹前promise實例狀態變爲「完成」狀態時,resolveFn函數自動執行。
噹噹前promise實例狀態變爲「失敗」狀態時,rejectFn函數自動執行。

2.2 promise Promise.prototype.catch( rejectFn )

@param rejectFn( ...args )  
    函數,當Promise實例狀態變爲「失敗」狀態時會被執行,  
    用於將從當前promise中取出reject( ...args )中獲得的參數(...args),  
    並進行相應的操做,好比將(args)傳入另外一個封裝了promise構造器的函數,  
    並將該函數執行完成後返回的promise實例返回  
    @param ...args  
      參數列表,當前promise處於「完成」狀態時,經過resolve(...args)獲得的值。  
  @return promise  
    promise對象,rejectFn執行後的返回值,  
    若是fn()返回的不是一個promise對象,catch()會幫咱們將fn()返回值封裝成promise對象,  
    並將其返回,以確保promise可以被繼續鏈式調用下去。

該方法實際上是「.then(null, rejectFn)」的別名,用於指定狀態轉爲「失敗」時的回調函數。
建議不要在then()方法中定義第二個參數,而應該使用catch(),結構層次會更好一些。
若是沒有使用catch()方法指定錯誤錯誤處理的回調函數,promise實例拋出的錯誤不會傳遞到外層代碼。
若是promise狀態已經變爲了resolved(「失敗」狀態),再拋出任何錯誤,都是無效的。
promise實例中拋出的錯誤具備冒泡的特性,它會一直向後傳遞,直到被捕獲爲止。

2.3 Promise.all( [promise1, promise2, ..., promisen] )

@param [promise1, promise2, ..., promisen]
    可遍歷對象,一個由promise對象構成的可遍歷對象,經常使用數組表示
  @return promise
    promise對象

Promise.all()用於將多個Promise實例包裝成一個新的Promise實例,並返回。
Promise.all()方法接受一個由Promise實例組成的可遍歷對象。若是可遍歷對象中存在有不是Promise實例的元素,就會調用Promise.resolve()方法,將其轉爲Promise實例。
本文的可遍歷對象,指的是那些具備Iterator接口的對象,如Array、WeakSet、Map、Set、WeakMap等函數的實例。
Promise.all()方法返回的Promise實例的狀態分紅兩種狀況:

  • 可遍歷對象中的Promise實例狀態全變爲 完成 狀態時,該實例的狀態纔會轉變爲 完成 狀態,此時,可遍歷對象中的Promise實例的返回值會組成一個數組,傳給該實例的回調。

  • 可遍歷對象只要存在Promise實例狀態轉爲 失敗 狀態時,該實例的狀態就會轉變爲 失敗 狀態,此時,第一個轉爲 失敗 狀態的Promise實例的返回值會傳給該實例的回調。

2.4 Promise.race( [promise1, promise2, ..., promisen] )

@param [promise1, promise2, ..., promisen]
    可遍歷對象,一個由promise對象構成的可遍歷對象,經常使用數組表示
  @return promise
    promise對象

Promise.race()與Promise.all()用法基本上一致,功能上也幾乎相同,惟一的差別就是:
Promise.race()方法返回的Promise實例的狀態分紅兩種狀況:

  • 可遍歷對象只要存在Promise實例狀態轉爲 完成 狀態時,該實例的狀態纔會轉變爲 完成 狀態,此時,第一個轉爲 完成 狀態的Promise實例的返回值,會做爲該實例的then()方法的回調函數的參數。

  • 可遍歷對象只要存在Promise實例狀態轉爲 失敗 狀態時,該實例的狀態就會轉變爲 失敗 狀態,此時,第一個轉爲 失敗 狀態的Promise實例的返回值,會做爲該實例的then()方法的回調函數的參數。

2.5 promise Promise.resolve( notHaveThenMethodObject )

@param notHaveThenMethodObject
    對象,一個原型鏈上不具備then()方法的對象
  @return promise
    promise對象

若是Promise.resolve()的參數的原型鏈上不具備then方法,則返回一個新的Promise實例,且其狀態爲 完成 狀態,而且會將它的參數做爲該實例的then()方法的回調函數的參數。
若是Promise.resolve()的參數是一個Promise實例(原型鏈上具備then方法),則將其原封不動地返回。
Promise.resolve()方法容許調用時不使用任何參數。

2.6 promise Promise.reject( something )

@param something
    任意值,用於傳遞給返回值的then()方法的回調函數參數的值
  @return promise
    promise對象

Promise.reject方法的用法和resolve方法基本同樣,只是它返回的Promise實例,狀態都是 失敗 狀態。
Promise.reject方法的參數會被做爲該實例的then()方法的回調函數的參數。
Promise.resolve()方法容許調用時不使用任何參數。

Promise構造器回調函數參數中的 resolvereject 和Promise構造器方法中的 reject()resolve() 效果是不同的。
Promise構造器回調函數參數中的 resolvereject 用於更改當前Promise的狀態,並將其值返回給當前Promise的then()方法的參數。
Promise構造器方法中的 reject()resolve() 能夠直接返回一個已經改變狀態的新的Promise對象。

  • Promise.reject() Promise.resolve()

  • new Promise((resolve, reject)=>{ resolve(...) 或 reject(...) })

2.7 Promise.prototype.done( [resolveFn], [rejectFn] )

@param [resolveFn( ...args )]  
    函數,可選,當Promise實例狀態變爲「完成」狀態時會被執行,  
    用於將從當前promise中取出reresolve( ...args )中獲得的參數(...args),  
    並進行相應的操做,好比將(args)傳入另外一個封裝了promise構造器的函數,  
    並將該函數執行完成後返回的promise實例返回  
    @param ...args  
      參數列表,當前promise實例處於「完成」狀態時,經過resolve(...args)獲得的值。  
  @param [rejectFn( ...args )]  
    函數,可選,當Promise實例狀態變爲「失敗」狀態時會被執行,  
    用於將從當前promise中取出reject( ...args )中獲得的參數(...args),  
    並進行相應的操做,好比將(args)傳入另外一個封裝了promise構造器的函數,  
    並將該函數執行完成後返回的promise實例返回  
    @param ...args  
      參數列表,當前promise處於「完成」狀態時,經過resolve(...args)獲得的值。

無論以then()或catch()方法結尾,若最後一個方法拋出錯誤,則在內部可能沒法捕捉到該錯誤,外界也沒法得到,爲了不這種狀況發生,Promise構造器的原型鏈上提供了done()方法。
promise.done()方法老是處於會調鏈的低端,它能夠捕捉到任何在回調鏈上拋出的錯誤,並將其拋出。

2.8 Promise.prototype.finally( simpleFn )

@param simpleFn  
    一個普通函數,這個普通函數不管如何都會被執行。

finally方法指定,無論Promise對象最後狀態如何,都會執行的操做。


3. 代碼參考

3.1 finally()的實現

Promise.prototype.finally = function( simpleFn ){
    let Pro = this.constructor
    return this.then(
      value => Pro.resolve( simpleFn() ).then( () => value ),
      error => Pro.resolve( simpleFn() ).then( () => { throw error } )
    )
  }

3.2 done()的實現

Promise.prototype.done = function( resolveFn, rejectFn ){
    this
      .then( resolveFn, rejectFn )
      .catch( error => {
        // 這是一個把須要執行的代碼,從任務隊列中拉出來的技巧
        setTimeout( () => { throw error }, 0)
      } )
  }

這兒使用了一個很經常使用的技巧:
咱們來看一下這個例子:

for(let i of [1,2,3]){
    setTimeout( () => { console.log( 'setTimeout ' + i ) }, 0)
    console.log( 'console ' + i )
  }

最終結果是:

> console 1  
  > console 2  
  > console 3  
  > undefined  
  > setTimeout 1  
  > setTimeout 2  
  > setTimeout 3

javascript除了維護着當前任務隊列,還維護着一個setTimeout隊列。全部未被執行的setTimeout任務,會按順序放到setTimeout隊列中,等待普通任務隊列中的任務執行完,纔開始按順序執行積累在setTimeout中的任務。
簡而言之, javascript會在執行完當前任務隊列中的任務後,再執行setTimeout隊列中的任務
咱們設置任務在0s後執行,能夠將該任務調到setTimeout隊列中,延遲該任務發生,使之異步執行。
這是異步執行方案當中,最經常使用,也最省時省事的一種方式。

3.3 加載圖片

function preloadImage(path){
    return new Promise( (resolve, reject) => {
      let img = document.createElement('img')
      img.style.display = 'none'
      document.body.appendChild(img)
      // 當圖片加載完成後,promise轉爲完成狀態
      // 此時,咱們能夠把該節點的圖片加載在應有的地方,而且將其刪除
      img.addEventListener('load', resolve)
      // 當圖片加載出錯後,promise轉爲失敗狀態
      img.addEventListener('error', reject)
      img.src = path
    } )
  }

3.4 Generator與Promise聯合

// Promise的包裝函數 getFoo()
  function getFoo(){
    // ......something
    return new Promise( (resolve, reject) => {
      // ......something
      resolve('foo')
    } )
  }
  // Generator函數 generator()
  function* generator(){
    try{
      let foo = yield getFoo()
      console.log(foo)
    }
    catch(error){
      console.log(error)
    }
  }
  // 自動執行generator函數的函數,如今能夠用async語法替代它
  function run(generator){
    // 讓generator函數運行至第一個yield語句前,
    // 並得到getFoo()的結果---一個promise函數
    let it = generator()
    function go(result){
      if(result.done) return result.value
      return result.value.then( value => {
          // 利用尾遞歸來實現自動執行,讓本次遞歸產生的棧單元項只有一個
          return go( it.next(value) )
        }, error => {
          return go( it.throw(error) )
        }
      )
    }
    go(it.next())
  }
  // 調用run方法
  run(generator)

本文更新信息
  1. 易杭 2017/4/24 23:10 11840字

本文做者信息
  1. 易杭 歡迎你們參觀個人博客個人Github

支持網站列表
  1. 易杭網 [www.freeedit.cn]

本文知識參考
  1. ES6標準入門第二版(阮一峯)

相關文章
相關標籤/搜索