手寫 PromiseA+ 實現,輕鬆透過 872 條用例

顾平安發表於2024-12-27

手寫 Promise/A+ 實現,輕鬆透過 872 條用例

規範參考:Promise/A+ 規範 - 中文版本

測試工具:https://github.com/promises-aplus/promises-tests

前言

從接觸 Promise 到現在,筆者經歷了這麼個過程:

  1. 瞭解各種 Promise 規範,包括 Promise/A+,但對其具體內容不甚瞭解。
  2. 研究前人的 Promise 實現,自己嘗試編寫,但測試時經常遇到問題。
  3. 苦思冥想,為什麼返回值能夠決定 Promise 的狀態?為什麼鏈式呼叫要返回一個新的 Promise 而不是 this?我陷入了深深的困惑。
  4. 可能是由於執著,我深入研讀了 Promise/A+ 規範。儘管起初對一些條款感到困惑,但我依然堅持學習。
  5. 逐句翻譯、理解規範,逐漸熟悉了它。從最初需要對照規範編寫程式碼,到如今能夠手寫 Promise,這個過程充滿了樂趣,也讓我對 Promise 的一些常用方法有了更深的理解。

如果你也在學習 Promise,遇到困難,我建議你對照規範和程式碼來理解。

實現

在閱讀程式碼之前,你需要明白,程式碼中的註釋並非隨意新增,每條註釋都對應著 Promise/A+ 規範中的具體條款。

關於規範,它主要包括以下幾個部分:

  1. 專業術語,你可以簡單瞭解即可。
  2. 詳細規範,需要逐行理解,包括:
    • 2.1 Promise 的 3 種狀態:pending(待定)、fulfilled(已實現)、rejected(已拒絕)。
    • 2.2 Promise 的 then 方法的實現,不同狀態下應執行的操作。
    • 2.3 Promise 對某值的決策行為,也稱為 Promise 解決過程。

因此,你會在註釋中看到類似以下的標記:

// 2.1 (2)(2)

它表示,規範對應的 2.1 Promise 狀態 下的第 2 個序號下的 第 2 條內容

標題序號 內容序號...

下方程式碼中儘管沒有涵蓋所有規範條款的註釋,但是隱式實現了。

完整程式碼如下:

function MyPromise(executor) {
  this.state = 'pending' // 2.1 (1)(1) 初始狀態,可以轉變為其它兩種狀態,也就是已實現(fulfilled)或已拒絕(rejected)
  this.value = null // 2.1 (2)(2) 必須有一個不可改變的值
  this.reason = null // 2.1 (3)(2) 必須有一個不可改變的原因
  this.onFulfilledCallbacks = []
  this.onRejectedCallbacks = []

  const resolve = (value) => {
    if (this.state !== 'pending') return
    this.state = 'fulfilled'
    this.value = value
    // 2.2 (6)(1) 當 promise 被實現時,所有相應的 onFulfilled 回撥必須按它們呼叫 then 的順序執行
    this.onFulfilledCallbacks.forEach((callback) => callback(this.value))
  }

  const reject = (reason) => {
    if (this.state !== 'pending') return
    this.state = 'rejected'
    this.reason = reason
    // 2.2 (6)(2) 當 promise 被拒絕時,所有相應的 onRejected 回撥必須按它們呼叫 then 的順序執行
    this.onRejectedCallbacks.forEach((callback) => callback(this.reason))
  }

  // 如果 then 中對返回的 Promise 執行器做了異常處理,此步可選
  try {
    executor(resolve, reject)
  } catch (e) {
    reject(e)
  }
}

// 2.2 (6) 原型上方法,根據不同狀態保證同一個 Promise 上的 then 可被多次呼叫
MyPromise.prototype.then = function (onFulfilled, onRejected) {
  // 2.2 (1) onFulfilled 和 onRejected 引數可選,若不是函式則忽略(此處略微改造,本質上還是符合規範的,返回值或丟擲異常決策鏈呼叫)
  const realOnFulfilled =
    typeof onFulfilled === 'function'
      ? onFulfilled // 2.2 (2)
      : (value) => {
          return value
        }
  const realOnRejected =
    typeof onRejected === 'function'
      ? onRejected // 2.2 (3)
      : (reason) => {
          throw reason
        }

  // 2.2 (7) then 必須返回一個 promise
  let promise2 = new MyPromise((resolve, reject) => {
    if (this.state === 'fulfilled') {
      // 2.2 (4) 必須在執行上下文棧僅包含平臺程式碼時才被呼叫
      queueMicrotask(() => {
        try {
          // 2.2 (5) 必須作為函式呼叫
          let x = realOnFulfilled(this.value)
          // 2.2 (7)(1) 根據返回值 x 執行 Promise 解決過程 [[Resolve]](promise2, x),此處也內含了規範 2.2 (7)(3) 處理
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          // 2.2 (7)(2) 丟擲異常 e,則 promise2 必須以 e 作為原因被拒絕,此處也包含了 2.2 (7)(4) 的處理(非函式時上方預設函式丟擲 reason,這裡捕捉拒絕,不就是實現了嗎)
          reject(e)
        }
      })
    } else if (this.state === 'rejected') {
      queueMicrotask(() => {
        try {
          let x = realOnRejected(this.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      })
    } else if (this.state === 'pending') {
      // 如果 Promise 中的非同步執行在後,then新增在前,該步驟能保證回撥不被忽略,參考觀察者 or 釋出訂閱?

      this.onFulfilledCallbacks.push(() => {
        queueMicrotask(() => {
          try {
            let x = realOnFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      })

      this.onRejectedCallbacks.push(() => {
        queueMicrotask(() => {
          try {
            let x = realOnRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      })
    }
  })

  // 2.2 (7)
  return promise2
}

function resolvePromise(promise, x, resolve, reject) {
  // 2.3 (1) 如果 promise 和 x 指向同一個物件,則以 TypeError 拒絕 promise 作為原因,這裡丟擲或者 return reject 均可
  if (promise === x)
    throw new TypeError('promise and return value are the same')

  // 根據 Promise 或者 thenable 物件特性,可以直接判斷分支如下
  if (x !== null && (typeof x === 'function' || typeof x === 'object')) {
    // 2.3 (3)(3)(3) 和 2.3 (3)(4)(1) 呼叫優先問題專用變數
    let called = false

    try {
      const then = x.then
      if (typeof then === 'function') {
      // 2.3 (3)(3) 如果 then 是一個函式,則以 x 作為 this 呼叫它
      then.call(
          x,
          (y) => {
            if (called) return
            called = true
            // 2.3 (3)(3)(1)
            resolvePromise(promise, y, resolve, reject)
          },
          (r) => {
            if (called) return
            called = true
            // 2.3 (3)(3)(2)
            reject(r)
          }
        )
      } else {
        // 2.3 (3)(4) x 為非 thenable 物件(如果 then 不是一個函式,則用 x 來實現 promise)
        resolve(x)
      }
    } catch (e) {
      if (called) return
      called = true
      // 2.3 (3)(2) 如果檢索屬性 x.then 時丟擲異常 e,則以 e 作為原因拒絕 promise
      // 2.3 (3)(3)(4)(2) 如果呼叫 then 時丟擲異常,以 e 作為原因拒絕 promise
      reject(e)
    }
  } else {
    // 2.3 (4) 如果 x 既不是物件也不是函式,則用 x 來實現 promise
    // x 為 null undefined、基本數值等情況
    resolve(x)
  }
}

// 暴露一個介面提供測試的靜態方法
MyPromise.deferred = function () {
  const result = {}
  result.promise = new MyPromise((resolve, reject) => {
    result.resolve = resolve
    result.reject = reject
  })
  return result
}

module.exports = MyPromise

相關文章