剖析Promise內部結構,一步一步實現一個完整的、能經過全部Test case的Promise類

本文寫給有必定Promise使用經驗的人,若是你尚未使用過Promise,這篇文章可能不適合你,建議先瞭解Promise的使用html

Promise標準解讀

1.只有一個then方法,沒有catch,race,all等方法,甚至沒有構造函數html5

Promise標準中僅指定了Promise對象的then方法的行爲,其它一切咱們常見的方法/函數都並無指定,包括catch,race,all等經常使用方法,甚至也沒有指定該如何構造出一個Promise對象,另外then也沒有通常實現中(Q, $q等)所支持的第三個參數,通常稱onProgressjava

2.then方法返回一個新的Promisegit

Promise的then方法返回一個新的Promise,而不是返回this,此處在下文會有更多解釋es6

promise2 = promise1.then(alert)
promise2 != promise1 // true

3.不一樣Promise的實現須要能夠相互調用(interoperable)github

4.Promise的初始狀態爲pending,它能夠由此狀態轉換爲fulfilled(本文爲了一致把此狀態叫作resolved)或者rejected,一旦狀態肯定,就不能夠再次轉換爲其它狀態,狀態肯定的過程稱爲settleweb

5.更具體的標準見這裏npm

一步一步實現一個Promise

下面咱們就來一步一步實現一個Promise數組

構造函數

由於標準並無指定如何構造一個Promise對象,因此咱們一樣以目前通常Promise實現中通用的方法來構造一個Promise對象,也是ES6原生Promise裏所使用的方式,即:promise

// Promise構造函數接收一個executor函數,executor函數執行完同步或異步操做後,調用它的兩個參數resolve和reject
var promise = new Promise(function(resolve, reject) {
  /*
    若是操做成功,調用resolve並傳入value
    若是操做失敗,調用reject並傳入reason
  */
})

咱們先實現構造函數的框架以下:

function Promise(executor) {
  var self = this
  self.status = 'pending' // Promise當前的狀態
  self.data = undefined  // Promise的值
  self.onResolvedCallback = [] // Promise resolve時的回調函數集,由於在Promise結束以前有可能有多個回調添加到它上面
  self.onRejectedCallback = [] // Promise reject時的回調函數集,由於在Promise結束以前有可能有多個回調添加到它上面

  executor(resolve, reject) // 執行executor並傳入相應的參數
}

上面的代碼基本實現了Promise構造函數的主體,但目前還有兩個問題:

1.咱們給executor函數傳了兩個參數:resolve和reject,這兩個參數目前尚未定義

2.executor有可能會出錯(throw),相似下面這樣,而若是executor出錯,Promise應該被其throw出的值reject:

new Promise(function(resolve, reject) {
  throw 2
})

因此咱們須要在構造函數裏定義resolve和reject這兩個函數:

function Promise(executor) {
  var self = this
  self.status = 'pending' // Promise當前的狀態
  self.data = undefined  // Promise的值
  self.onResolvedCallback = [] // Promise resolve時的回調函數集,由於在Promise結束以前有可能有多個回調添加到它上面
  self.onRejectedCallback = [] // Promise reject時的回調函數集,由於在Promise結束以前有可能有多個回調添加到它上面

  function resolve(value) {
    // TODO
  }

  function reject(reason) {
    // TODO
  }

  try { // 考慮到執行executor的過程當中有可能出錯,因此咱們用try/catch塊給包起來,而且在出錯後以catch到的值reject掉這個Promise
    executor(resolve, reject) // 執行executor
  } catch(e) {
    reject(e)
  }
}

有人可能會問,resolve和reject這兩個函數能不能不定義在構造函數裏呢?考慮到咱們在executor函數裏是以resolve(value),reject(reason)的形式調用的這兩個函數,而不是以resolve.call(promise, value),reject.call(promise, reason)這種形式調用的,因此這兩個函數在調用時的內部也必然有一個隱含的this,也就是說,要麼這兩個函數是通過bind後傳給了executor,要麼它們定義在構造函數的內部,使用self來訪問所屬的Promise對象。因此若是咱們想把這兩個函數定義在構造函數的外部,確實是能夠這麼寫的:

function resolve() {
  // TODO
}
function reject() {
  // TODO
}
function Promise(executor) {
  try {
    executor(resolve.bind(this), reject.bind(this))
  } catch(e) {
    reject.bind(this)(e)
  }
}

可是衆所周知,bind也會返回一個新的函數,這麼一來仍是至關於每一個Promise對象都有一對屬於本身的resolve和reject函數,就跟寫在構造函數內部沒什麼區別了,因此咱們就直接把這兩個函數定義在構造函數裏面了。不過話說回來,若是瀏覽器對bind的所優化,使用後一種形式應該能夠提高一下內存使用效率。

另外咱們這裏的實現並無考慮隱藏this上的變量,這使得這個Promise的狀態能夠在executor函數外部被改變,在一個靠譜的實現裏,構造出的Promise對象的狀態和最終結果應當是沒法從外部更改的。

接下來,咱們實現resolve和reject這兩個函數

function Promise(executor) {
  // ...

  function resolve(value) {
    if (self.status === 'pending') {
      self.status = 'resolved'
      self.data = value
      for(var i = 0; i < self.onResolvedCallback.length; i++) {
        self.onResolvedCallback[i](value)
      }
    }
  }

  function reject(reason) {
    if (self.status === 'pending') {
      self.status = 'rejected'
      self.data = reason
      for(var i = 0; i < self.onRejectedCallback.length; i++) {
        self.onRejectedCallback[i](reason)
      }
    }
  }

  // ...
}

基本上就是在判斷狀態爲pending以後把狀態改成相應的值,並把對應的value和reason存在self的data屬性上面,以後執行相應的回調函數,邏輯很簡單,這裏就很少解釋了。

then方法

Promise對象有一個then方法,用來註冊在這個Promise狀態肯定後的回調,很明顯,then方法須要寫在原型鏈上。then方法會返回一個Promise,關於這一點,Promise/A+標準並無要求返回的這個Promise是一個新的對象,但在Promise/A標準中,明確規定了then要返回一個新的對象,目前的Promise實現中then幾乎都是返回一個新的Promise(詳情)對象,因此在咱們的實現中,也讓then返回一個新的Promise對象。

關於這一點,我認爲標準中是有一點矛盾的:

標準中說,若是promise2 = promise1.then(onResolved, onRejected)裏的onResolved/onRejected返回一個Promise,則promise2直接取這個Promise的狀態和值爲己用,但考慮以下代碼:

promise2 = promise1.then(function foo(value) {
  return Promise.reject(3)
})

此處若是foo運行了,則promise1的狀態必然已經肯定且爲resolved,若是then返回了this(即promise2 === promise1),說明promise2和promise1是同一個對象,而此時promise1/2的狀態已經肯定,沒有辦法再取Promise.reject(3)的狀態和結果爲己用,由於Promise的狀態肯定後就不可再轉換爲其它狀態。

另外每一個Promise對象均可以在其上屢次調用then方法,而每次調用then返回的Promise的狀態取決於那一次調用then時傳入參數的返回值,因此then不能返回this,由於then每次返回的Promise的結果都有可能不一樣。

下面咱們來實現then方法:

// then方法接收兩個參數,onResolved,onRejected,分別爲Promise成功或失敗後的回調
Promise.prototype.then = function(onResolved, onRejected) {
  var self = this
  var promise2

  // 根據標準,若是then的參數不是function,則咱們須要忽略它,此處以以下方式處理
  onResolved = typeof onResolved === 'function' ? onResolved : function(v) {}
  onRejected = typeof onRejected === 'function' ? onRejected : function(r) {}

  if (self.status === 'resolved') {
    return promise2 = new Promise(function(resolve, reject) {

    })
  }

  if (self.status === 'rejected') {
    return promise2 = new Promise(function(resolve, reject) {

    })
  }

  if (self.status === 'pending') {
    return promise2 = new Promise(function(resolve, reject) {

    })
  }
}

Promise總共有三種可能的狀態,咱們分三個if塊來處理,在裏面分別都返回一個new Promise。

根據標準,咱們知道,對於以下代碼,promise2的值取決於then裏面函數的返回值:

promise2 = promise1.then(function(value) {
  return 4
}, function(reason) {
  throw new Error('sth went wrong')
})

若是promise1被resolve了,promise2的將被4 resolve,若是promise1被reject了,promise2將被new Error('sth went wrong') reject,更多複雜的狀況再也不詳述。

因此,咱們須要在then裏面執行onResolved或者onRejected,並根據返回值(標準中記爲x)來肯定promise2的結果,而且,若是onResolved/onRejected返回的是一個Promise,promise2將直接取這個Promise的結果:

Promise.prototype.then = function(onResolved, onRejected) {
  var self = this
  var promise2

  // 根據標準,若是then的參數不是function,則咱們須要忽略它,此處以以下方式處理
  onResolved = typeof onResolved === 'function' ? onResolved : function(value) {}
  onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {}

  if (self.status === 'resolved') {
    // 若是promise1(此處即爲this/self)的狀態已經肯定而且是resolved,咱們調用onResolved
    // 由於考慮到有可能throw,因此咱們將其包在try/catch塊裏
    return promise2 = new Promise(function(resolve, reject) {
      try {
        var x = onResolved(self.data)
        if (x instanceof Promise) { // 若是onResolved的返回值是一個Promise對象,直接取它的結果作爲promise2的結果
          x.then(resolve, reject)
        }
        resolve(x) // 不然,以它的返回值作爲promise2的結果
      } catch (e) {
        reject(e) // 若是出錯,以捕獲到的錯誤作爲promise2的結果
      }
    })
  }

  // 此處與前一個if塊的邏輯幾乎相同,區別在於所調用的是onRejected函數,就再也不作過多解釋
  if (self.status === 'rejected') {
    return promise2 = new Promise(function(resolve, reject) {
      try {
        var x = onRejected(self.data)
        if (x instanceof Promise) {
          x.then(resolve, reject)
        }
      } catch (e) {
        reject(e)
      }
    })
  }

  if (self.status === 'pending') {
  // 若是當前的Promise還處於pending狀態,咱們並不能肯定調用onResolved仍是onRejected,
  // 只能等到Promise的狀態肯定後,才能確實如何處理。
  // 因此咱們須要把咱們的**兩種狀況**的處理邏輯作爲callback放入promise1(此處即this/self)的回調數組裏
  // 邏輯自己跟第一個if塊內的幾乎一致,此處不作過多解釋
    return promise2 = new Promise(function(resolve, reject) {
      self.onResolvedCallback.push(function(value) {
        try {
          var x = onResolved(self.data)
          if (x instanceof Promise) {
            x.then(resolve, reject)
          }
        } catch (e) {
          reject(e)
        }
      })

      self.onRejectedCallback.push(function(reason) {
        try {
          var x = onRejected(self.data)
          if (x instanceof Promise) {
            x.then(resolve, reject)
          }
        } catch (e) {
          reject(e)
        }
      })
    })
  }
}

// 爲了下文方便,咱們順便實現一個catch方法
Promise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}

至此,咱們基本實現了Promise標準中所涉及到的內容,但還有幾個問題:

1.不一樣的Promise實現之間須要無縫的可交互,即Q的Promise,ES6的Promise,和咱們實現的Promise之間以及其它的Promise實現,應該而且是有必要無縫相互調用的,好比:

// 此處用MyPromise來表明咱們實現的Promise
new MyPromise(function(resolve, reject) { // 咱們實現的Promise
  setTimeout(function() {
    resolve(42)
  }, 2000)
}).then(function() {
  return new Promise.reject(2) // ES6的Promise
}).then(function() {
  return Q.all([ // Q的Promise
    new MyPromise(resolve=>resolve(8)), // 咱們實現的Promise
    new Promise.resolve(9), // ES6的Promise
    Q.resolve(9) // Q的Promise
  ])
})

咱們前面實現的代碼並無處理這樣的邏輯,咱們只判斷了onResolved/onRejected的返回值是否爲咱們實現的Promise的實例,並無作任何其它的判斷,因此上面這樣的代碼目前是沒有辦法在咱們的Promise里正確運行的。

2.下面這樣的代碼目前也是沒辦法處理的:

new Promise(resolve=>resolve(8))
  .then()
  .then()
  .then(function foo(value) {
    alert(value)
  })

正確的行爲應該是alert出8,而若是拿咱們的Promise,運行上述代碼,將會alert出undefined。這種行爲稱爲穿透,即8這個值會穿透兩個then(說Promise更爲準確)到達最後一個then裏的foo函數裏,成爲它的實參,最終將會alert出8。

下面咱們首先處理簡單的狀況,值的穿透

Promise值的穿透

經過觀察,會發現咱們但願下面這段代碼

new Promise(resolve=>resolve(8))
  .then()
  .catch()
  .then(function(value) {
    alert(value)
  })

跟下面這段代碼的行爲是同樣的

new Promise(resolve=>resolve(8))
  .then(function(value){
    return value
  })
  .catch(function(reason){
    throw reason
  })
  .then(function(value) {
    alert(value)
  })

因此若是想要把then的實參留空且讓值能夠穿透到後面,意味着then的兩個參數的默認值分別爲function(value) {return value},function(reason) {throw reason}。
因此咱們只須要把then裏判斷onResolved和onRejected的部分改爲以下便可:

onResolved = typeof onResolved === 'function' ? onResolved : function(value) {return value}
onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {throw reason}

因而Promise神奇的值的穿透也沒有那麼黑魔法,只不過是then默認參數就是把值日後傳或者拋

不一樣Promise的交互

關於不一樣Promise間的交互,其實標準裏是有說明的,其中詳細指定了如何經過then的實參返回的值來決定promise2的狀態,咱們只須要按照標準把標準的內容轉成代碼便可。

這裏簡單解釋一下標準:

即咱們要把onResolved/onRejected的返回值,x,當成一個多是Promise的對象,也即標準裏所說的thenable,並以最保險的方式調用x上的then方法,若是你們都按照標準實現,那麼不一樣的Promise之間就能夠交互了。而標準爲了保險起見,即便x返回了一個帶有then屬性但並不遵循Promise標準的對象(好比說這個x把它then裏的兩個參數都調用了,同步或者異步調用(PS,原則上then的兩個參數須要異步調用,下文會講到),或者是出錯後又調用了它們,或者then根本不是一個函數),也能儘量正確處理。

關於爲什麼須要不一樣的Promise實現可以相互交互,我想緣由應該是顯然的,Promise並非JS一早就有的標準,不一樣第三方的實現之間是並不相互知曉的,若是你使用的某一個庫中封裝了一個Promise實現,想象一下若是它不能跟你本身使用的Promise實現交互的場景。。。

建議各位對照着標準閱讀如下代碼,由於標準對此說明的很是詳細,因此你應該可以在任意一個Promise實現中找到相似的代碼:

/*
resolvePromise函數即爲根據x的值來決定promise2的狀態的函數
也即標準中的[Promise Resolution Procedure](https://promisesaplus.com/#point-47)
x爲`promise2 = promise1.then(onResolved, onRejected)`裏`onResolved/onRejected`的返回值
`resolve`和`reject`其實是`promise2`的`executor`的兩個實參,由於很難掛在其它的地方,因此一併傳進來。
相信各位必定能夠對照標準把標準轉換成代碼,這裏就只標出代碼在標準中對應的位置,只在必要的地方作一些解釋
*/
function resolvePromise(promise2, x, resolve, reject) {
  var then
  var thenCalledOrThrow = false

  if (promise2 === x) { // 對應標準2.3.1節
    return reject(new TypeError('Chaining cycle detected for promise!'))
  }

  if (x instanceof Promise) { // 對應標準2.3.2節
    // 若是x的狀態尚未肯定,那麼它是有可能被一個thenable決定最終狀態和值的
    // 因此這裏須要作一下處理,而不能一律的覺得它會被一個「正常」的值resolve
    if (x.status === 'pending') {
      x.then(function(value) {
        resolvePromise(promise2, value, resolve, reject)
      }, reject)
    } else { // 但若是這個Promise的狀態已經肯定了,那麼它確定有一個「正常」的值,而不是一個thenable,因此這裏直接取它的狀態
      x.then(resolve, reject)
    }
    return
  }

  if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) { // 2.3.3
    try {

      // 2.3.3.1 由於x.then有多是一個getter,這種狀況下屢次讀取就有可能產生反作用
      // 即要判斷它的類型,又要調用它,這就是兩次讀取
      then = x.then 
      if (typeof then === 'function') { // 2.3.3.3
        then.call(x, function rs(y) { // 2.3.3.3.1
          if (thenCalledOrThrow) return // 2.3.3.3.3 即這三處誰選執行就以誰的結果爲準
          thenCalledOrThrow = true
          return resolvePromise(promise2, y, resolve, reject) // 2.3.3.3.1
        }, function rj(r) { // 2.3.3.3.2
          if (thenCalledOrThrow) return // 2.3.3.3.3 即這三處誰選執行就以誰的結果爲準
          thenCalledOrThrow = true
          return reject(r)
        })
      } else { // 2.3.3.4
        resolve(x)
      }
    } catch (e) { // 2.3.3.2
      if (thenCalledOrThrow) return // 2.3.3.3.3 即這三處誰選執行就以誰的結果爲準
      thenCalledOrThrow = true
      return reject(e)
    }
  } else { // 2.3.4
    resolve(x)
  }
}

而後咱們使用這個函數的調用替換then裏幾處判斷x是否爲Promise對象的位置便可,見下方完整代碼。

最後,咱們剛剛說到,原則上,promise.then(onResolved, onRejected)裏的這兩相函數須要異步調用,關於這一點,標準裏也有說明

In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack.

因此咱們須要對咱們的代碼作一點變更,即在四個地方加上setTimeout(fn, 0),這點會在完整的代碼中註釋,請各位自行發現。

事實上,即便你不參照標準,最終你在自測試時也會發現若是then的參數不以異步的方式調用,有些狀況下Promise會不按預期的方式行爲,經過不斷的自測,最終你必然會讓then的參數異步執行,讓executor函數當即執行。本人在一開始實現Promise時就沒有參照標準,而是本身憑經驗測試,最終發現的這個問題。

至此,咱們就實現了一個的Promise,完整代碼以下:

try {
  module.exports = Promise
} catch (e) {}

function Promise(executor) {
  var self = this

  self.status = 'pending'
  self.onResolvedCallback = []
  self.onRejectedCallback = []

  function resolve(value) {
    if (value instanceof Promise) {
      return value.then(resolve, reject)
    }
    setTimeout(function() { // 異步執行全部的回調函數
      if (self.status === 'pending') {
        self.status = 'resolved'
        self.data = value
        for (var i = 0; i < self.onResolvedCallback.length; i++) {
          self.onResolvedCallback[i](value)
        }
      }
    })
  }

  function reject(reason) {
    setTimeout(function() { // 異步執行全部的回調函數
      if (self.status === 'pending') {
        self.status = 'rejected'
        self.data = reason
        for (var i = 0; i < self.onRejectedCallback.length; i++) {
          self.onRejectedCallback[i](reason)
        }
      }
    })
  }

  try {
    executor(resolve, reject)
  } catch (reason) {
    reject(reason)
  }
}

function resolvePromise(promise2, x, resolve, reject) {
  var then
  var thenCalledOrThrow = false

  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise!'))
  }

  if (x instanceof Promise) {
    if (x.status === 'pending') { //because x could resolved by a Promise Object
      x.then(function(v) {
        resolvePromise(promise2, v, resolve, reject)
      }, reject)
    } else { //but if it is resolved, it will never resolved by a Promise Object but a static value;
      x.then(resolve, reject)
    }
    return
  }

  if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
    try {
      then = x.then //because x.then could be a getter
      if (typeof then === 'function') {
        then.call(x, function rs(y) {
          if (thenCalledOrThrow) return
          thenCalledOrThrow = true
          return resolvePromise(promise2, y, resolve, reject)
        }, function rj(r) {
          if (thenCalledOrThrow) return
          thenCalledOrThrow = true
          return reject(r)
        })
      } else {
        resolve(x)
      }
    } catch (e) {
      if (thenCalledOrThrow) return
      thenCalledOrThrow = true
      return reject(e)
    }
  } else {
    resolve(x)
  }
}

Promise.prototype.then = function(onResolved, onRejected) {
  var self = this
  var promise2
  onResolved = typeof onResolved === 'function' ? onResolved : function(v) {
    return v
  }
  onRejected = typeof onRejected === 'function' ? onRejected : function(r) {
    throw r
  }

  if (self.status === 'resolved') {
    return promise2 = new Promise(function(resolve, reject) {
      setTimeout(function() { // 異步執行onResolved
        try {
          var x = onResolved(self.data)
          resolvePromise(promise2, x, resolve, reject)
        } catch (reason) {
          reject(reason)
        }
      })
    })
  }

  if (self.status === 'rejected') {
    return promise2 = new Promise(function(resolve, reject) {
      setTimeout(function() { // 異步執行onRejected
        try {
          var x = onRejected(self.data)
          resolvePromise(promise2, x, resolve, reject)
        } catch (reason) {
          reject(reason)
        }
      })
    })
  }

  if (self.status === 'pending') {
    // 這裏之因此沒有異步執行,是由於這些函數必然會被resolve或reject調用,而resolve或reject函數裏的內容已經是異步執行,構造函數裏的定義
    return promise2 = new Promise(function(resolve, reject) {
      self.onResolvedCallback.push(function(value) {
        try {
          var x = onResolved(value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (r) {
          reject(r)
        }
      })

      self.onRejectedCallback.push(function(reason) {
          try {
            var x = onRejected(reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (r) {
            reject(r)
          }
        })
    })
  }
}

Promise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}

Promise.deferred = Promise.defer = function() {
  var dfd = {}
  dfd.promise = new Promise(function(resolve, reject) {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}

測試

如何肯定咱們實現的Promise符合標準呢?Promise有一個配套的測試腳本,只須要咱們在一個CommonJS的模塊中暴露一個deferred方法(即exports.deferred方法),就能夠了,代碼見上述代碼的最後。而後執行以下代碼便可執行測試:

npm i -g promises-aplus-tests
promises-aplus-tests Promise.js

關於Promise的其它問題

Promise的性能問題

可能各位看官會以爲奇怪,Promise能有什麼性能問題呢?並無大量的計算啊,幾乎都是處理邏輯的代碼。

理論上說,不能叫作「性能問題」,而只是有可能出現的延遲問題。什麼意思呢,記得剛剛咱們說須要把4塊代碼包在setTimeout裏吧,先考慮以下代碼:

var start = +new Date()
function foo() {
  setTimeout(function() {
    console.log('setTimeout')
    if((+new Date) - start < 1000) {
      foo()
    }
  })
}
foo()

運行上面的代碼,會打印出多少次'setTimeout'呢,各位能夠本身試一下,不出意外的話,應該是250次左右,我剛剛運行了一次,是241次。這說明,上述代碼中兩次setTimeout運行的時間間隔約是4ms(另外,setInterval也是同樣的),實事上,這正是瀏覽器兩次Event Loop之間的時間間隔,相關標準各位能夠自行查閱。另外,在Node中,這個時間間隔跟瀏覽器不同,通過個人測試,是1ms。

單單一個4ms的延遲可能在通常的web應用中並不會有什麼問題,可是考慮極端狀況,咱們有20個Promise鏈式調用,加上代碼運行的時間,那麼這個鏈式調用的第一行代碼跟最後一行代碼的運行極可能會超過100ms,若是這之間沒有對UI有任何更新的話,雖然本質上沒有什麼性能問題,但可能會形成必定的卡頓或者閃爍,雖然在web應用中這種情形並不常見,可是在Node應用中,確實是有可能出現這樣的case的,因此一個可以應用於生產環境的實現有必要把這個延遲消除掉。在Node中,咱們能夠調用process.nextTick或者setImmediate(Q就是這麼作的),在瀏覽器中具體如何作,已經超出了本文的討論範圍,總的來講,就是咱們須要實現一個函數,行爲跟setTimeout同樣,但它須要異步且儘早的調用全部已經加入隊列的函數,這裏有一個實現。

如何中止一個Promise鏈?

在一些場景下,咱們可能會遇到一個較長的Promise鏈式調用,在某一步中出現的錯誤讓咱們徹底沒有必要去運行鏈式調用後面全部的代碼,相似下面這樣(此處略去了then/catch裏的函數):

new Promise(function(resolve, reject) {
  resolve(42)
})
  .then(function(value) {
    // "Big ERROR!!!"
  })
  .catch()
  .then()
  .then()
  .catch()
  .then()

假設這個Big ERROR!!!的出現讓咱們徹底沒有必要運行後面全部的代碼了,但鏈式調用的後面即有catch,也有then,不管咱們是return仍是throw,都不可避免的會進入某一個catch或then裏面,那有沒有辦法讓這個鏈式調用在Big ERROR!!!的後面就停掉,徹底不去執行鏈式調用後面全部回調函數呢?

一開始遇到這個問題的時候我也百思不得其解,在網上搜遍了也沒有結果,有人說能夠在每一個catch裏面判斷Error的類型,若是本身處理不了就接着throw,也有些其它辦法,但老是要對現有代碼進行一些改動而且全部的地方都要遵循這些約定,甚是麻煩。

然而當我從一個實現者的角度看問題時,確實找到了答案,就是在發生Big ERROR後return一個Promise,但這個Promise的executor函數什麼也不作,這就意味着這個Promise將永遠處於pending狀態,因爲then返回的Promise會直接取這個永遠處於pending狀態的Promise的狀態,因而返回的這個Promise也將一直處於pending狀態,後面的代碼也就一直不會執行了,具體代碼以下:

new Promise(function(resolve, reject) {
  resolve(42)
})
  .then(function(value) {
    // "Big ERROR!!!"
    return new Promise(function(){})
  })
  .catch()
  .then()
  .then()
  .catch()
  .then()

這種方式看起來有些山寨,它也確實解決了問題。但它引入的一個新問題就是鏈式調用後面的全部回調函數都沒法被垃圾回收器回收(在一個靠譜的實現裏,Promise應該在執行完全部回調後刪除對全部回調函數的引用以讓它們能被回收,在前文的實現裏,爲了減小複雜度,並無作這種處理),但若是咱們不使用匿名函數,而是使用函數定義或者函數變量的話,在須要屢次執行的Promise鏈中,這些函數也都只有一份在內存中,不被回收也是能夠接受的。

咱們能夠將返回一個什麼也不作的Promise封裝成一個有語義的函數,以增長代碼的可讀性:

Promise.cancel = Promise.stop = function() {
  return new Promise(function(){})
}

而後咱們就能夠這麼使用了:

new Promise(function(resolve, reject) {
  resolve(42)
})
  .then(function(value) {
    // "Big ERROR!!!"
    return Promise.stop()
  })
  .catch()
  .then()
  .then()
  .catch()
  .then()

看起來是否是有語義的多?

Promise鏈上返回的最後一個Promise出錯了怎麼辦?

考慮以下代碼:

new Promise(function(resolve) {
  resolve(42)
})
  .then(function(value) {
    alter(value)
  })

乍一看好像沒什麼問題,但運行這段代碼的話你會發現什麼現象也不會發生,既不會alert出42,也不會在控制檯報錯,怎麼回事呢。細看最後一行,alert被打成了alter,那爲何控制檯也沒有報錯呢,由於alter所在的函數是被包在try/catch塊裏的,alter這個變量找不到就直接拋錯了,這個錯就正好成了then返回的Promise的rejection reason。

也就是說,在Promise鏈的最後一個then裏出現的錯誤,很是難以發現,有文章指出,能夠在全部的Promise鏈的最後都加上一個catch,這樣出錯後就能被捕獲到,這種方法確實是可行的,可是首先在每一個地方都加上幾乎相同的代碼,違背了DRY原則,其次也至關的繁瑣。另外,最後一個catch依然返回一個Promise,除非你能保證這個catch裏的函數再也不出錯,不然問題依然存在。在Q中有一個方法叫done,把這個方法鏈到Promise鏈的最後,它就可以捕獲前面未處理的錯誤,這其實跟在每一個鏈後面加上catch沒有太大的區別,只是由框架來作了這件事,至關於它提供了一個不會出錯的catch鏈,咱們能夠這麼實現done方法:

Promise.prototype.done = function(){
  return this.catch(function(e) { // 此處必定要確保這個函數不能再出錯
    console.error(e)
  })
}

但是,能不能在不加catch或者done的狀況下,也可以讓開發者發現Promise鏈最後的錯誤呢?答案依然是確定的。

咱們能夠在一個Promise被reject的時候檢查這個Promise的onRejectedCallback數組,若是它爲空,則說明它的錯誤將沒有函數處理,這個時候,咱們須要把錯誤輸出到控制檯,讓開發者能夠發現。如下爲具體實現:

function reject(reason) {
  setTimeout(function() {
    if (self.status === 'pending') {
      self.status = 'rejected'
      self.data = reason
      if (self.onRejectedCallback.length === 0) {
        console.error(reason)
      }
      for (var i = 0; i < self.rejectedFn.length; i++) {
        self.rejectedFn[i](reason)
      }
    }
  })
}

上面的代碼對於如下的Promise鏈也能處理的很好:

new Promise(function(){ // promise1
  reject(3)
})
  .then() // returns promise2
  .then() // returns promise3
  .then() // returns promise4

看起來,promise1,2,3,4都沒有處理函數,那是否是會在控制檯把這個錯誤輸出4次呢,並不會,實際上,promise1,2,3都隱式的有處理函數,就是then的默認參數,各位應該還記得then的默認參數最終是被push到了Promise的callback數組裏。只有promise4是真的沒有任何callback,由於壓根就沒有調用它的then方法。

事實上,Bluebird和ES6 Promise都作了相似的處理,在Promise被reject但又沒有callback時,把錯誤輸出到控制檯。

Q使用了done方法來達成相似的目的,$q在最新的版本中也加入了相似的功能。

Angular裏的$q跟其它Promise的交互

通常來講,咱們不會在Angular裏使用其它的Promise,由於Angular已經集成了$q,但有些時候咱們在Angular裏須要用到其它的庫(好比LeanCloud的JS SDK),而這些庫或是封裝了ES6的Promise,或者是本身實現了Promise,這時若是你在Angular裏使用這些庫,就有可能發現視圖跟Model不一樣步。究其緣由,是由於$q已經集成了Angular的digest loop機制,在Promise被resolve或reject時觸發digest,而其它的Promise顯然是不會集成的,因此若是你運行下面這樣的代碼,視圖是不會同步的:

app.controller(function($scope) {
  Promise.resolve(42).then(function(value) {
    $scope.value = value
  })
})

Promise結束時並不會觸發digest,因此視圖沒有同步。$q上正好有個when方法,它能夠把其它的Promise轉換成$q的Promise(有些Promise實現中提供了Promise.cast函數,用於將一個thenable轉換爲它的Promise),問題就解決了:

app.controller(function($scope, $q) {
  $q.when(Promise.resolve(42)).then(function(value) {
    $scope.value = value
  })
})

固然也有其它的解決方案好比在其它Promise的鏈的最後加一個digest,相似下面這樣:

Promise.prototype.$digest = function() {
  $rootScope.$digest()
  return this
}
// 而後這麼使用
OtherPromise
  .resolve(42)
  .then(function(value) {
    $scope.value = value
  })
  .$digest()

由於使用場景並很少,此處不作深刻討論。

出錯時,是用throw new Error()仍是用return Promise.reject(new Error())呢?

這裏我以爲主要從性能和編碼的溫馨度角度考慮:

性能方面,throw new Error()會使代碼進入catch塊裏的邏輯(還記得咱們把全部的回調都包在try/catch裏了吧),傳說throw用多了會影響性能,由於一但throw,代碼就有可能跳到不可預知的位置。

但考慮到onResolved/onRejected函數是直接被包在Promise實現裏的try裏,出錯後就直接進入了這個try對應 的catch塊,代碼的跳躍「幅度」相對較小,我認爲這裏的性能損失能夠忽略不記。有機會能夠測試一下。

而使用Promise.reject(new Error()),則須要構造一個新的Promise對象(裏面包含2個數組,4個函數:resolve/reject,onResolved/onRejected),也會花費必定的時間和內存。

而從編碼溫馨度的角度考慮,出錯用throw,正常時用return,能夠比較明顯的區分出錯與正常,throw和return又同爲關鍵字,用來處理對應的狀況也顯得比較對稱(-_-)。另外在通常的編輯器裏,Promise.reject不會被高亮成與throw和return同樣的顏色。最後,若是開發者又不喜歡構造出一個Error對象的話,Error的高亮也沒有了。

綜上,我以爲在Promise裏發現顯式的錯誤後,用throw拋出錯誤會比較好,而不是顯式的構造一個被reject的Promise對象。

最佳實踐

這裏難免再囉嗦兩句最佳實踐

1.一是不要把Promise寫成嵌套結構,至於怎麼改進,這裏就很少說了

// 錯誤的寫法
promise1.then(function(value) {
  promise1.then(function(value) {
    promise1.then(function(value) {

    })
  })
})

2.二是鏈式Promise要返回一個Promise,而不僅是構造一個Promise

// 錯誤的寫法
Promise.resolve(1).then(function(){
  Promise.resolve(2)
}).then(function(){
  Promise.resolve(3)
})

Promise相關的convenience method的實現

請到這裏查看Promise.race, Promise.all, Promise.resolve, Promise.reject等方法的具體實現,這裏就不具體解釋了,總的來講,只要then的實現是沒有問題的,其它全部的方法均可以很是方便的依賴then來實現。

結語

最後,若是你以爲這篇文章對你有所幫助,歡迎分享給你的朋友或者團隊,記得註明出處哦~

原文連接:https://github.com/xieranmaya/blog/issues/3

相關文章
相關標籤/搜索