手動實現(xiàn)Promise

前言

眾所周知,ES6中的Promise對象是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件,更合理和強大,目前也得到了前端領(lǐng)域的廣泛使用。不僅是項目實踐中常用,面試中也經(jīng)常出現(xiàn)。
Promise的使用想必大家都很熟練,可究其內(nèi)部原理,很多人都是一知半解。這導(dǎo)致面試中出現(xiàn)Promise原理甚至要求手動封裝時,很多人都會掛掉。本著知其然,也要知其所以然的目的,開始對Promise進行了探索。


一、為什么要使用Promise

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

setTimeout(function () {
    console.log('延時觸發(fā)');
}, 2000);

fs.readFile('./sample.txt', 'utf-8', function (err, res) {
    console.log(res);
});

上面就是典型的回調(diào)函數(shù),不論是在瀏覽器中,還是在node中,JavaScript本身是單線程,因此,為了應(yīng)對一些單線程帶來的問題,異步編程成為了JavaScript中非常重要的一部分。
不論是瀏覽器中最為常見的ajax、事件監(jiān)聽,還是node中文件讀取、網(wǎng)絡(luò)編程、數(shù)據(jù)庫等操作,都離不開異步編程。在異步編程中,許多操作都會放在回調(diào)函數(shù)(callback)中。同步與異步的混雜、過多的回調(diào)嵌套都會使得代碼變得難以理解與維護,這也是常受人詬病的地方。
回調(diào)嵌套過多后,你的代碼會變成這樣:

asyncFunc1(opt, (...args1) => {
    asyncFunc2(opt, (...args2) => {
        asyncFunc3(opt, (...args3) => {
            asyncFunc4(opt, (...args4) => {
                // some operation
            });
        });
    });
});

左側(cè)明顯出現(xiàn)了一個三角形的縮進區(qū)域,過多的回調(diào)也就讓我們陷入“回調(diào)地獄”。
雖然回調(diào)地獄可以通過減少嵌套、模塊化等方式來解決,但我們有更好的方案可以采取,那就是Promise。

二、含義與規(guī)范

Promise 是一個對象,保存著異步操作的結(jié)果,在異步操作結(jié)束后,會變更 Promise 的狀態(tài),然后調(diào)用注冊在 then 方法上回調(diào)函數(shù)。
實際上,Promise 是對 Promises/A+ 規(guī)范的一種實現(xiàn)。 ES6 原生提供了 Promise 對象,統(tǒng)一了用法。

三、封裝Promise

下面將依據(jù)規(guī)范和ES6來進行Promise的封裝

1.promise構(gòu)造函數(shù)

規(guī)范沒有指明如何書寫構(gòu)造函數(shù),那就參考ES6的構(gòu)造方式:

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

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

Promise 構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別是 resolve 和 reject

resolve 函數(shù)的作用是將 Promise 對象的狀態(tài)從 pending 變?yōu)?fulfilled ,在異步操作成功時調(diào)用,并將異步操作的結(jié)果,作為參數(shù)傳遞給注冊在 then 方法上的回調(diào)函數(shù)(then方法的第一個參數(shù)); reject 函數(shù)的作用是將 Promise 對象的狀態(tài)從 pending 變?yōu)?rejected ,在異步操作失敗時調(diào)用,并將異步操作報出的錯誤,作為參數(shù)傳遞給注冊在 then 方法上的回調(diào)函數(shù)(then方法的第二個參數(shù))

所以我們要實現(xiàn)的 promise (小寫以便區(qū)分ES6的 Promise )構(gòu)造函數(shù)大體如下:

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

  // 2.1
  function resolve (value) {
    // 將promise的狀態(tài)從pending更改為fulfilled,并且以value為參數(shù)依次調(diào)用then方法中注冊的回調(diào)
    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的狀態(tài)從pending更改為rejected,并且以reason為參數(shù)依次調(diào)用then方法中注冊的回調(diào)
    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)
}

規(guī)范2.2.6中明確指明 then 方法可以被同一個 promise 對象調(diào)用,所以這里需要用一個數(shù)組 onFulfilledCb 來存儲then方法中注冊的回調(diào)

這里我們執(zhí)行 resolve reject 內(nèi)部代碼使用setTimeout,是為了確保 then 方法上注冊的回調(diào)能異步執(zhí)行(規(guī)范3.1)

2.then方法

promise 實例具有 then 方法,也就是說,then方法是定義在原型對象 promise.prototype 上的。它的作用是為 promise 實例添加狀態(tài)改變時的回調(diào)函數(shù)。

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

閱讀理解規(guī)范2.1和2.2,我們也很容易對then方法進行實現(xiàn):

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 函數(shù)的返回值x如何處理,規(guī)范中提到一個概念叫 PromiseResolutionProcedure ,這里我們就叫做Promise解決過程

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

3.promise解決過程

對照規(guī)范2.3,我們再來實現(xiàn) promise resolution , promise resolution 針對x的類型做了各種處理:如果 promise 和 x 指向同一對象,以 TypeError 為 reason 拒絕執(zhí)行 promise、如果 x 為 promise ,則使 promise 接受 x 的狀態(tài)、如果 x 為對象或者函數(shù),判斷 x.then 是否是函數(shù)、 如果 x 不為對象或者函數(shù),以 x 為參數(shù)執(zhí)行 promise(resolve和reject參數(shù)攜帶promise2的作用域,方便在x狀態(tài)變更后去更改promise2的狀態(tài))

// 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)
  }
}
4.思考

以上,基本實現(xiàn)了一個簡易版的 promise ,說白了,就是對 Promises/A+ 規(guī)范的一個翻譯,將規(guī)范翻譯成代碼。因為大家的實現(xiàn)都是基于這個規(guī)范,所以不同的 promise 實現(xiàn)之間能夠共存(不得不說制定規(guī)范的人才是最厲害的)

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)

ES7 的 Async/Await 也是基于 promise 來實現(xiàn)的,可以理解成 async 函數(shù)會隱式地返回一個 Promise , await 后面的執(zhí)行代碼放到 then 方法中

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

5.極簡版promise

考慮到上述實現(xiàn)方法還是太全面,完全依照規(guī)范來寫的,不直觀且難懂。下面寫一個極簡版的實現(xiàn)方式,以便大家理解和在面試時使用:

function promise () {
  this.status = 'pending' // 2.1
  this.msg = '' // 存儲value與reason
  let process = arguments[0],
       that = this
  process (function () {
    that.status = 'resolve'
    that.msg = argument[0]
  }, function () {
    that.status = 'reject'
    that.msg = argument[0]
  })
  return this
}

promise.prototype.then = function () {
  if (this.status === 'resolve') {
    arguments[0](this.msg)
  } else if (this.status === 'reject' && arguments[1]) {
    arguments[1](this.msg)
  }
}

四、promise的弊端

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

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

五、總結(jié)

支持 promise 的庫有很多,現(xiàn)在主流的瀏覽器也都原生支持 promise 了,而且還有更好用的 Async/Await 。希望大家都有所收獲!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容