帶你進入Promise探討

歡迎大家的討論學習
在這裏插入圖片描述

Promise

一、前言

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

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))
        }
      })
    })
  }
})

雖然回調地獄可以通過減少嵌套、模塊化等方式來解決,但我們有更好的方案可以採取,那就是 Promise

二、含義

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

三、實現

Promise 的使用想必大家都很熟練,可是究其內部原理,在這之前,我一直是一知半解。本着知其然,也要知其所以然的目的,開始對 Promise 的實現產生了興趣。

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

1、promise構造函數

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

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

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

Promise 構造函數接受一個函數作爲參數,該函數的兩個參數分別是 resolve 和 reject

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

所以我們要實現的 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.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.3、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)

2、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.1、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)
        }
      })
    })
  }
}

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

Promise 解決過程是一個抽象的操作,需要輸入一個 promise 和一個值,我們表示爲 [[Resolve]](promise, x),如果 x 有 then 方法且看上去像一個 Promise ,解決程序即嘗試使 promise 接受 x 的狀態;否則用 x 的值來執行 promise

3、 promise解決過程

對照規範2.3,我們再來實現 promise resolution , promise resolution 針對x的類型做了各種處理:如果 promise 和 x 指向同一對象,以 TypeError 爲 reason 拒絕執行 promise、如果 x 爲 promise ,則使 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)
  }
}

完整代碼

// 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.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.3、2.2.6
        that.onRejectedCb.map(item => {
          item(that.reason)
        })
      }
    }, 0)
  }

  fn(resolve, reject)
}

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

  // 2.2.1、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)
          promiseResolution(promise2, x, resolve, reject)
        } catch(e) {
          // 2.2.7.2
          reject(e)
        }
      })

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

// 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)
  }
}

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

function doSomethingElse() {
  // return this.promise2

  // return new promise((resolve, reject) => {
  //   setTimeout(() => {
  //     resolve('promise2 done')
  //   }, 1000)
  // })

  // return {
  //   then: (resolvePromise, rejectPromise) => {
  //     setTimeout(() => {
  //       resolvePromise(22)
  //       rejectPromise(33)
  //     })
  //   }
  // }

  // return { test: 4 }

  // return function() {
  //   console.log('xxxx')
  // }

  // return 4

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

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

四、思考

以上,基本實現了一個簡易版的 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)


至於 ES6 的 finally 、 all 等常用方法,規範雖然沒有制定,但是藉助 then 方法,我們實現起來也很方便stage-5

ES7 的 Async/Await 也是基於 promise 來實現的,可以理解成 async 函數會隱式地返回一個 Promise , await 後面的執行代碼放到 then 方法中

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

五、promise的弊端

promise徹底解決了callback hell,但也存在以下一些問題

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

六、總結

支持 promise 的庫有很多,現在主流的瀏覽器也都原生支持 promise 了,而且還有更好用的 Async/Await 。之所以還要花精力去寫這篇文章,道理很簡單,就是想對規範有一個更深的理解,希望看到這裏的同學同樣也能有所收穫。
喜歡的同學可以關注我哦。
在這裏插入圖片描述
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章