Promise探討

1、前言

你們都知道JavaScript一大特色就是單線程,爲了避免阻塞主線程,有些耗時操做(好比ajax)必須放在任務隊列中異步執行。傳統的異步編程解決方案之一回調,很容易產生臭名昭著的回調地獄問題。html

fs.readdir(source, function(err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function(filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function(err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function(width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})

雖然回調地獄能夠經過減小嵌套、模塊化等方式來解決,但咱們有更好的方案能夠採起,那就是 Promisegit

2、含義

Promise 是一個對象,保存着異步操做的結果,在異步操做結束後,會變動 Promise 的狀態,而後調用註冊在 then 方法上回調函數。 ES6 原生提供了 Promise 對象,統一用法(具體可參考阮一峯的ES6入門es6

3、實現

Promise 的使用想必你們都很熟練,但是究其內部原理,在這以前,我一直是隻知其一;不知其二。本着知其然,也要知其因此然的目的,開始對 Promise 的實現產生了興趣。github

衆所周知,Promise 是對 Promises/A+ 規範的一種實現,那咱們首先得了解規範,
詳情請看Promise/A+規範,我的github上有對應的中文翻譯README.mdajax

promise構造函數

規範沒有指明如何書寫構造函數,那就參考下 ES6 的構造方式編程

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 異步操做成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise 構造函數接受一個函數做爲參數,該函數的兩個參數分別是 resolvereject數組

resolve 函數的做用是將 Promise 對象的狀態從 pending 變爲 fulfilled ,在異步操做成功時調用,並將異步操做的結果,做爲參數傳遞給註冊在 then 方法上的回調函數(then方法的第一個參數); reject 函數的做用是將 Promise 對象的狀態從 pending 變爲 rejected ,在異步操做失敗時調用,並將異步操做報出的錯誤,做爲參數傳遞給註冊在 then 方法上的回調函數(then方法的第二個參數)promise

因此咱們要實現的 promise (小寫以便區分ES6的Promise )構造函數大致以下:瀏覽器

// promise 構造函數
function promise(fn) {
  let that = this
  that.status = 'pending' // 存儲promise的state
  that.value = '' // 存儲promise的value
  that.reason = '' // 存儲promise的reason
  that.onFulfilledCb = [] // 存儲then方法中註冊的回調函數(第一個參數)
  that.onRejectedCb = [] // 存儲then方法中註冊的回調函數(第二個參數)

  // 2.1
  function resolve(value) {
    // 將promise的狀態從pending更改成fulfilled,而且以value爲參數依次調用then方法中註冊的回調
    setTimeout(() => {
      if (that.status === 'pending') {
        that.status = 'fulfilled'
        that.value = value
        // 2.2.二、2.2.6
        that.onFulfilledCb.map(item => {
          item(that.value)
        })
      }
    }, 0)
  }

  function reject(reason) {
    // 將promise的狀態從pending更改成rejected,而且以reason爲參數依次調用then方法中註冊的回調
    setTimeout(() => {
      if (that.status === 'pending') {
        that.status = 'rejected'
        that.reason = reason
        // 2.2.三、2.2.6
        that.onRejectedCb.map(item => {
          item(that.reason)
        })
      }
    }, 0)
  }

  fn(resolve, reject)
}

規範2.2.6中明確指明 then 方法能夠被同一個 promise 對象調用,因此這裏須要用一個數組 onFulfilledCb 來存儲then方法中註冊的回調異步

這裏咱們執行 resolve reject 內部代碼使用setTimeout,是爲了確保 then 方法上註冊的回調能異步執行(規範3.1)

then方法

promise 實例具備 then 方法,也就是說,then 方法是定義在原型對象 promise.prototype 上的。它的做用是爲 promise 實例添加狀態改變時的回調函數。

規範2.2promise 必須提供一個 then 方法 promise.then(onFulfilled, onRejected)
規範2.2.7 then 方法必須返回一個新的promise

閱讀理解規範2.1和2.2,咱們也很容易對then方法進行實現:

promise.prototype.then = function(onFulfilled, onRejected) {
  let that = this
  let promise2

  // 2.2.一、2.2.5
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
  onRejected = typeof onRejected === 'function' ? onRejected : r => r

  if (that.status === 'pending') {
    // 2.2.7
    return promise2 = new promise((resolve, reject) => {
      that.onFulfilledCb.push(value => {
        try {
          let x = onFulfilled(value)
        } catch(e) {
          // 2.2.7.2
          reject(e)
        }
      })

      that.onRejectedCb.push(reason => {
        try {
          let x = onRejected(reason)
        } catch(e) {
          // 2.2.7.2
          reject(e)
        }
      })
    })
  }
}

重點在於對 onFulfilledonRejected 函數的返回值x如何處理,規範中提到一個概念叫
Promise Resolution Procedure ,這裏咱們就叫作Promise解決過程

Promise 解決過程是一個抽象的操做,須要輸入一個 promise 和一個值,咱們表示爲 [[Resolve]](promise, x),若是 xthen 方法且看上去像一個 Promise ,解決程序即嘗試使 promise 接受 x 的狀態;不然用 x 的值來執行 promise

promise解決過程

對照規範2.3,咱們再來實現 promise resolutionpromise resolution 針對x的類型作了各類處理:若是 promisex 指向同一對象,以 TypeErrorreason 拒絕執行 promise、若是 xpromise ,則使 promise 接受 x 的狀態、若是 x 爲對象或者函數,判斷 x.then 是不是函數、 若是 x 不爲對象或者函數,以 x 爲參數執行 promise(resolve和reject參數攜帶promise2的做用域,方便在x狀態變動後去更改promise2的狀態)

// promise resolution
function promiseResolution(promise2, x, resolve, reject) {
  let then
  let thenCalled = false
  // 2.3.1
  if (promise2 === x) {
    return reject(new TypeError('promise2 === x is not allowed'))
  }
  // 2.3.2
  if (x instanceof promise) {
    x.then(resolve, reject)
  }
  // 2.3.3
  if (typeof x === 'object' || typeof x === 'function') {
    try {
      // 2.3.3.1
      then = x.then
      if (typeof then === 'function') {
        // 2.3.3.2
        then.call(x, function resolvePromise(y) {
          // 2.3.3.3.3
          if (thenCalled) return
          thenCalled = true
          // 2.3.3.3.1
          return promiseResolution(promise2, y, resolve, reject)
        }, function rejectPromise(r) {
          // 2.3.3.3.3
          if (thenCalled) return
          thenCalled = true
          // 2.3.3.3.2
          return reject(r)
        })
      } else {
        // 2.3.3.4
        resolve(x)
      }
    } catch(e) {
      // 2.3.3.3.4.1
      if (thenCalled) return
      thenCalled = true
      // 2.3.3.2
      reject(e)
    }
  } else {
    // 2.3.4
    resolve(x)
  }
}

完整代碼可查看stage-4

思考

以上,基本實現了一個簡易版的 promise ,說白了,就是對 Promises/A+ 規範的一個翻譯,將規範翻譯成代碼。由於你們的實現都是基於這個規範,因此不一樣的 promise 實現之間可以共存(不得不說制定規範的人才是最厲害的)

function doSomething() {
  return new promise((resolve, reject) => {
    setTimeout(() => {
      resolve('promise done')
    }, 2000)
  })
}

function doSomethingElse() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('ES6 promise')
    }, 1000)
  })
}

this.promise2 = doSomething().then(doSomethingElse)
console.log(this.promise2)

至於 ES6finallyall 等經常使用方法,規範雖然沒有制定,可是藉助 then 方法,咱們實現起來也很方便stage-5

ES7Async/Await 也是基於 promise 來實現的,能夠理解成 async 函數會隱式地返回一個 Promiseawait 後面的執行代碼放到 then 方法中

更深層次的思考,你須要理解規範中每一條制定的意義,好比爲何then方法不像jQuery那樣返回this而是要從新返回一個新的promise對象(若是then返回了this,那麼promise2就和promise1的狀態同步,promise1狀態變動後,promise2就沒辦法接受後面異步操做進行的狀態變動)、 promise解決過程 中爲何要規定 promise2x 不能指向同一對象(防止循環引用)

promise的弊端

promise完全解決了callback hell,但也存在如下一些問題

  1. 延時問題(涉及到evnet loop)
  2. promise一旦建立,沒法取消
  3. pending狀態的時候,沒法得知進展到哪一步(好比接口超時,能夠藉助race方法)
  4. promise會吞掉內部拋出的錯誤,不會反映到外部。若是最後一個then方法裏出現錯誤,沒法發現。(能夠採起hack形式,在promise構造函數中判斷onRejectedCb的數組長度,若是爲0,就是沒有註冊回調,這個時候就拋出錯誤,某些庫實現done方法,它不會返回一個promise對象,且在done()中未經處理的異常不會被promise實例所捕獲)
  5. then方法每次調用都會建立一個新的promise對象,必定程度上形成了內存的浪費

    總結

    支持 promise 的庫有不少,如今主流的瀏覽器也都原生支持 promise 了,並且還有更好用的 Async/Await 。之因此還要花精力去寫這篇文章,道理很簡單,就是想對規範有一個更深的理解,但願看到這裏的同窗一樣也能有所收穫

相關文章
相關標籤/搜索