ES6 Promise物件

易杭發表於2017-04-24

ES6 Promise物件


本文更新資訊
  1. 易杭 2017/4/24 23:10 11840字
本文作者資訊
  1. 易杭
支援網站列表
  1. 易杭網 [www.freeedit.cn]
本文知識參考
  1. ES6標準入門第二版(阮一峰)

1. Promise本質

Promise本質上是一個 函式 ,更確切地說,它是一個 構造器 ,專門用來構造物件的。
它接受一個函式作為引數,並返回一個物件,大致情況如下:

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

Promise函式的引數,作為函式形式存在,需要我們手動去編寫。
它需要兩個引數,情況如下:

```javascript
  function fn(resolve, reject){
    ...  // 我們自己寫的邏輯程式碼
  }
```

Promise函式的返回值是一個物件,準確來說,是Promise自己生成的例項。
其實Promise函式的使命,就是構建出它的例項,並且負責幫我們管理這些例項。
該例項有三種狀態,分別是: 進行 狀態、 完成 狀態 和 失敗 狀態。
該例項只能從“ 進行 狀態”轉變為“ 完成 狀態”,或從“ 進行 狀態”轉變為“ 失敗 狀態”,這個過程不可逆轉,也不可能存在其他可能。因為Promise就是用來管理業務狀態的一種機制,它能夠保證業務的順序執行,而不出現混亂。

這就好比我們在家裡炒一份菜,是隻可能存在“ 正在炒菜 ”、“ 炒好了 ”和“ 炒糊了 ”這三個階段的,而“正在炒菜”的狀態肯定是會優先存在於“炒好了”和“炒糊了”兩個狀態前面,“炒好了”和“炒糊了”本身又是兩個 互斥的事件 ,所以這個過程,只可能出現從“正在炒菜”狀態過渡到“炒好了”或者“炒糊了”狀態的情況,永遠不可能從“炒好了”過渡到“炒糊了”狀態,也不可能從“炒糊了”過渡到“炒好了”狀態。

那麼,這些由Promise函式構建出來的物件,究竟有著什麼用處呢?
我們先來看一組程式碼:

```javascript
  fn( ( ( ( ()=>{} )=>{} )=>{} )=>{} )
```

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

其實,程式導向或物件導向的函數語言程式設計,本身就是一個巨大的“函式呼叫”過程。我們在程式碼中使用函式,並在函式中呼叫函式,執行環境幫助我們維護一個或多個函式棧,以實現程式的有序執行,及增強軟體後期維護的便利性。

但如果我們能把這種不斷呼叫的過程給攤開成 平面 ,而不要使函式相互巢狀,就會使我們的軟體可維護性提升很大一個臺階。我們只需要將原本寫好的功能一個個羅列出來,並構造出一根供函式呼叫的鏈條,把這些功能一個個地按需呼叫,軟體的功能不就實現了麼?而且還更清晰明瞭。
Promise幫助我們將函式攤開來,形成一根呼叫鏈條,讓程式有序執行。

每一個返回值為Promise例項的函式,都是 Promise呼叫鏈條上的一個結點 ,這個Promise例項維護著該處函式的執行狀態,並決定著自身的生存週期。它的寫法大致是這樣的:

```javascript
  // 執行一個返回值為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_”開頭的函式)到底長什麼樣。

```javascript
  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] )

```javascript
  @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 )

```javascript
  @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] )

```javascript
  @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] )

```javascript
  @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 )

```javascript
  @param notHaveThenMethodObject
    物件,一個原型鏈上不具有then()方法的物件
  @return promise
    promise物件
```

如果Promise.resolve()的引數的原型鏈上不具有then方法,則返回一個新的Promise例項,且其狀態為 完成 狀態,並且會將它的引數作為該例項的then()方法的回撥函式的引數。
如果Promise.resolve()的引數是一個Promise例項(原型鏈上具有then方法),則將其原封不動地返回。
Promise.resolve()方法允許呼叫時不使用任何引數。

2.6 promise Promise.reject( something )

```javascript
  @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] )

```javascript
  @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 )

```javascript
  @param simpleFn  
    一個普通函式,這個普通函式無論如何都會被執行。  
```

finally方法指定,不管Promise物件最後狀態如何,都會執行的操作。


3. 程式碼參考

3.1 finally()的實現

```javascript
  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()的實現

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

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

```javascript
  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 載入圖片

```javascript
  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聯合

```javascript
  // 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)
```

相關文章