按照 Promise/A+ 規範逐行註釋並實現 Promise

Asheng 發表於 2022-04-01

0. 前言

面試官:「你寫個 Promise 吧。」

我:「對不起,打擾了,再見!」

現在前端越來越卷,不會手寫 Promise 都不好意思面試了(手動狗頭.jpg)。雖然沒多少人會在業務中用自己實現的 Promise,但是,實現 Promise 的過程會讓你對 Promise 更加了解,出了問題也可以更好地排查。

如果你還不熟悉 Promise,建議先看一下 MDN 文件

在實現 Promise 之前,我建議你先看一遍 Promises/A+ 規範(中文翻譯:Promise A+ 規範),本文中不會再次介紹相關的概念。推薦大家優先閱讀原版英文,只需高中水平的英語知識就夠了,遇到不懂的再看譯文。

另外,本文將使用 ES6 中的 Class 來實現 Promise。為了方便大家跟 Promise/A+ 規範對照著看,下文的順序將按照規範的順序來行文。

在正式開始之前,我們新建一個專案,名稱隨意,按照以下步驟進行初始:

  • 開啟 CMD 或 VS Code,執行 npm init,初始化專案
  • 新建 PromiseImpl.js 檔案,後面所有的程式碼實現都寫在這個檔案裡

完整程式碼地址:ashengtan/promise-aplus-implementing

1. 術語

這部分大家直接看規範就好,也沒什麼好解釋的。注意其中對於 value 的描述,value 可以是一個 thenable(有 then 方法的物件或函式) 或者 Promise,這點會在後面的實現中體現出來。

2. 要求

2.1 Promise 的狀態

一個 Promise 有三種狀態:

  • pending:初始狀態
  • fulfilled:成功執行
  • rejected:拒絕執行

一個 Promise 一旦從 pending 變為 fulfilledrejected,就無法變成其他狀態。當 fulfilled 時,需要給出一個不可變的值;同樣,當 rejected 時,需要給出一個不可變的原因。

根據以上資訊,我們定義 3 個常量,用來表示 Promise 的狀態:

const STATUS_PENDING = 'pending'
const STATUS_FULFILLED = 'fulfilled'
const STATUS_REJECTED = 'rejected'

接著,我們先把 Promise 的基礎框架先定義出來,這裡我使用 ES6 的 Class 來定義:

class PromiseImpl {
  constructor() {}

  then(onFulfilled, onRejected) {}
}

這裡我們先回想一下 Promise 的基本用法:

const promise = new Promise((resolve, reject) => {
  // ...do something
  resolve(value) // or reject(error)
})

// 多次呼叫
const p1 = promise.then()
const p2 = promise.then()
const p3 = promise.then()

好了,繼續完善 PromiseImpl,先完善一下構造方法:

class PromiseImpl {
  constructor() {
    // `Promise` 當前的狀態,初始化時為 `pending`
    this.status = STATUS_PENDING
    // fulfilled 時的值
    this.value = null
    // rejected 時的原因
    this.reason = null
  }
}

另外,我們還要定義兩個方法,用於 fulfilledrejected 時回撥:

class PromiseImpl {
  constructor() {
    // ...其他程式碼

    // 2.1.2 When `fulfilled`, a `promise`:
    //  2.1.2.1 must not transition to any other state.
    //  2.1.2.2 must have a value, which must not change.
    const _resolve = value => {
      // 如果 `value` 是 `Promise`(即巢狀 `Promise`),
      // 則需要等待該 `Promise` 執行完成
      if (value instanceof PromiseImpl) {
        return value.then(
          value => _resolve(value),
          reason => _reject(reason)
        )
      }
      
      if (this.status === STATUS_PENDING) {
        this.status = STATUS_FULFILLED
        this.value = value
      }
    }

    // 2.1.3 When `rejected`, a `promise`:
    //  2.1.3.1 must not transition to any other state.
    //  2.1.3.2 must have a reason, which must not change.
    const _reject = reason => {
      if (this.status === STATUS_PENDING) {
        this.status = STATUS_REJECTED
        this.reason = reason
      }
    }
  }
}

注意,在 _resolve() 中,如果 valuePromise 的話(即巢狀 Promise),則需要等待該 Promise 執行完成。這點很重要,因為後面的其他 API 如 Promise.resolvePromise.allPromise.allSettled 等均需要等待巢狀 Promise 執行完成才會返回結果。

最後,別忘了在 new Promise() 時,我們需要將 resolvereject 傳給呼叫者:

class PromiseImpl {
  constructor(executor) {
    // ...其他程式碼

    try {
      executor(_resolve, _reject)
    } catch (e) {
      _reject(e)
    }
  }
}

使用 trycatchexecutor 包裹起來,因為這部分是呼叫者的程式碼,我們無法保證呼叫者的程式碼不會出錯。

2.2 Then 方法

一個 Promise 必須提供一個 then 方法,其接受兩個引數:

promise.then(onFulfilled, onRejected)
class PromiseImpl {
  then(onFulfilled, onRejected) {}
}

2.2.1 onFulfilledonRejected

從規範 2.2.1 中我們可以得知以下資訊:

  • onFulfilledonRejected 是可選引數
  • 如果 onFulfilledonRejected 不是函式,則必須被忽略

因此,我們可以這樣實現:

class PromiseImpl {
  then(onFulfilled, onRejected) {
    // 2.2.1 Both `onFulfilled` and `onRejected` are optional arguments:
    //   2.2.1.1 If `onFulfilled` is not a function, it must be ignored
    //   2.2.1.2 If `onRejected` is not a function, it must be ignored
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => {}
    onRejected = typeof onRejected === 'function' ? onRejected : () => {}
  }
}

2.2.2 onFulfilled 特性

從規範 2.2.2 中我們可以得知以下資訊:

  • 如果 onFulfilled 是一個函式,則必須在 fulfilled 後呼叫,第一個引數為 promise 的值
  • 只能呼叫一次
class PromiseImpl {
  then(onFulfilled, onRejected) {
    // ...其他程式碼
    
    // 2.2.2 If `onFulfilled` is a function:
    //   2.2.2.1 it must be called after `promise` is fulfilled,
    // with promise’s value as its first argument.
    //   2.2.2.2 it must not be called before `promise` is fulfilled.
    //   2.2.2.3 it must not be called more than once.
    if (this.status === STATUS_FULFILLED) {
      onFulfilled(this.value)
    }
  }
}

2.2.3 onRejected 特性

onFulfilled 同理,只不過是在 rejected 時呼叫,第一個引數為 promise 失敗的原因

class PromiseImpl {
  then(onFulfilled, onRejected) {
    // ...其他程式碼

    // 2.2.3 If onRejected is a function:
    //   2.2.3.1 it must be called after promise is rejected,
    // with promise’s reason as its first argument.
    //   2.2.3.2 it must not be called before promise is rejected.
    //   2.2.3.3 it must not be called more than once.
    if (this.status === STATUS_REJECTED) {
      onRejected(this.reason)
    }
  }
}

2.2.4 非同步執行

在日常開發中,我們經常使用 Promise 來做一些非同步操作,規範 2.2.4 就是規定非同步執行的問題,具體的可以結合規範裡的註釋閱讀,重點是確保 onFulfilledonRejected 要非同步執行。

需要指出的是,規範裡並沒有規定 Promise 一定要用 micro-task 機制來實現,因此你使用 macro-task 機制來實現也是可以的。當然,現在瀏覽器用的是 micro-task 來實現。這裡為了方便,我們使用 setTimeout(屬於 macro-task)來實現。

因此,我們需要稍微改造下上面的程式碼:

class PromiseImpl {
  then(onFulfilled, onRejected) {
    // ...其他程式碼
    
    // fulfilled
    if (this.status === STATUS_FULFILLED) {
      setTimeout(() => {
        onFulfilled(this.value)
      }, 0)
    }

    // rejected
    if (this.status === STATUS_REJECTED) {
      setTimeout(() => {
        onRejected(this.reason)
      }, 0)
    }
  }
}

2.2.5 onFulfilledonRejected 必須作為函式被呼叫

這個已經在上面實現過了。

2.2.6 then 可被多次呼叫

舉個例子:

const promise = new Promise((resolve, reject) => {
  // ...do something
  resolve(value) // or reject(error)
})

promise.then()
promise.then()
promise.catch()

因此,必須確保當 Promise fulfilledrejected 時,onFulfilledonRejected 按照其註冊的順序逐一回撥。還記得最開始我們定義的 resolvereject 嗎?這裡我們需要改造下,保證所有的回撥都被執行到:

const invokeArrayFns = (fns, arg) => {
  for (let i = 0; i < fns.length; i++) {
    fns[i](arg)
  }
}

class PromiseImpl {
  constructor(executor) {
    // ...其他程式碼

    // 用於存放 `fulfilled` 時的回撥,一個 `Promise` 物件可以註冊多個 `fulfilled` 回撥函式
    this.onFulfilledCbs = []
    // 用於存放 `rejected` 時的回撥,一個 `Promise` 物件可以註冊多個 `rejected` 回撥函式
    this.onRejectedCbs = []

    const resolve = value => {
      if (this.status === STATUS_PENDING) {
        this.status = STATUS_FULFILLED
        this.value = value
        // 2.2.6.1 If/when `promise` is fulfilled, 
        // all respective `onFulfilled` callbacks must execute 
        // in the order of their originating calls to `then`.
        invokeArrayFns(this.onFulfilledCbs, value)
      }
    }

    const reject = reason => {
      if (this.status === STATUS_PENDING) {
        this.status = STATUS_REJECTED
        this.reason = reason
        // 2.2.6.2 If/when `promise` is rejected, 
        // all respective `onRejected` callbacks must execute 
        // in the order of their originating calls to `then`.
        invokeArrayFns(this.onRejectedCbs, reason)
      }
    }
  }
}

看到這裡你可能會有疑問,什麼時候往 onFulfilledCbsonRejectedCbs 裡存放對應的回撥,答案是在呼叫 then 時:

class PromiseImpl {
  then(onFulfilled, onRejected) {
    // ...其他程式碼

    // pending
    if (this.status === STATUS_PENDING) {
      this.onFulfilledCbs.push(() => {
        setTimeout(() => {
          onFulfilled(this.value)
        }, 0)
      })

      this.onRejectedCbs.push(() => {
        setTimeout(() => {
          onRejected(this.reason)
        }, 0)
      })
    }
  }
}

此時 Promise 處於 pending 狀態,無法確定其最後是 fulfilled 還是 rejected,因此需要將回撥函式存放起來,待狀態確定後再執行相應的回撥函式。

注:invokeArrayFns 來源於 Vue.js 3 中的原始碼。

2.2.7 then 必須返回 Promise

promise2 = promise1.then(onFulfilled, onRejected)

那麼,在我們上面的程式碼中,then 怎麼才能返回 Promise 呢?很簡單:

class PromiseImpl {
  then(onFulfilled, onRejected) {
    let promise2 = new PromiseImpl((resolve, reject) => {
      if (this.status === STATUS_FULFILLED) {
        // ...相關程式碼
      }

      if (this.status === STATUS_REJECTED) {
        // ...相關程式碼
      }

      if (this.status === STATUS_PENDING) {
        // ...相關程式碼
      }
    })

    return promise2
  }
}

因為呼叫 then 之後返回一個新的 Promise 物件,使得我們也可以進行鏈式呼叫:

Promise.resolve(42).then().then()...

2.2.7.1 ~ 2.2.7.4 這四點比較重要,我們下面分別來看看。

2.2.7.1 如果 onFulfilledonRejected 返回一個值 x,則執行 Promise 解決過程,[[Resolve]](promise2, x)

解釋:其實所謂執行 Promise 解決過程就是執行某個操作,我們把這個操作抽取成一個方法,並命名為:promiseResolutionProcedure(promise, x, resolve, reject)。為了方便,我們把 resolvereject 一併透傳進去。

class PromiseImpl {
  then(onFulfilled, onRejected) {
    // ...其他程式碼
    
    let promise2 = new PromiseImpl((resolve, reject) => {
      if (this.status === STATUS_FULFILLED) {
        setTimeout(() => {
          // 2.2.7.1
          let x = onFulfilled(this.value)
          promiseResolutionProcedure(promise2, x, resolve, reject)
        }, 0)
      }

      if (this.status === STATUS_REJECTED) {
        setTimeout(() => {
          // 2.2.7.1
          let x = onRejected(this.reason)
          promiseResolutionProcedure(promise2, x, resolve, reject)
        }, 0)
      }

      if (this.status === STATUS_PENDING) {
        this.onFulfilledCbs.push(() => {
          setTimeout(() => {
            // 2.2.7.1 
            let x = onFulfilled(this.value)
            promiseResolutionProcedure(promise2, x, resolve, reject)
          }, 0)
        })

        this.onRejectedCbs.push(() => {
          setTimeout(() => {
            // 2.2.7.1
            let x = onRejected(this.reason)
            promiseResolutionProcedure(promise2, x, resolve, reject)
          }, 0)
        })
      }
    })

    return promise2
  }
}

2.2.7.2 如果 onFulfilledonRejected 丟擲一個異常 e,則 promise2 必須 rejected,並返回原因 e

解釋:實現上面體現在執行 onFulfilledonRejected 時使用 trycatch 包括起來,並在 catch 時呼叫 reject(e)

class PromiseImpl {
  then(onFulfilled, onRejected) {
    // ...其他程式碼
    
    let promise2 = new PromiseImpl((resolve, reject) => {
      if (this.status === STATUS_FULFILLED) {
        setTimeout(() => {
          try {
            // 2.2.7.1
            let x = onFulfilled(this.value)
            promiseResolutionProcedure(promise2, x, resolve, reject)
          } catch (e) {
            // 2.2.7.2
            reject(e)
          }
        }, 0)
      }

      if (this.status === STATUS_REJECTED) {
        setTimeout(() => {
          try {
            // 2.2.7.1
            let x = onRejected(this.reason)
            promiseResolutionProcedure(promise2, x, resolve, reject)
          } catch (e) {
            // 2.2.7.2
            reject(e)
          }
        }, 0)
      }

      if (this.status === STATUS_PENDING) {
        this.onFulfilledCbs.push(() => {
          setTimeout(() => {
            try {
              // 2.2.7.1
              let x = onFulfilled(this.value)
              promiseResolutionProcedure(promise2, x, resolve, reject)
            } catch (e) {
              // 2.2.7.2
              reject(e)
            }
          }, 0)
        })

        this.onRejectedCbs.push(() => {
          setTimeout(() => {
            try {
              // 2.2.7.1
              let x = onRejected(this.reason)
              promiseResolutionProcedure(promise2, x, resolve, reject)
            } catch (e) {
              // 2.2.7.2
              reject(e)
            }
          }, 0)
        })
      }
    })

    // 2.2.7 `then` must return a promise
    return promise2
  }
}

2.2.7.3 如果 onFulfilled 不是函式且 promise1 已經 fulfilled,則 promise2 必須 fulfilled 且返回與 promise1 相同的值。

解釋:值的透傳,例如:

Promise.resolve(42).then().then(value => console.log(value)) // 42

在第一個 then 時,我們忽略了 onFulfilled,那麼在鏈式呼叫的時候,需要把值透傳給後面的 then

class PromiseImpl {
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value

    // ...其他程式碼
  }
}

2.2.7.4 如果 onRejected 不是函式且 promise1 已經 rejected,則 promise2 必須 rejected 且返回與 promise1 相同的原因。

解釋:同理,原因也要透傳:

Promise.reject('reason').catch().catch(reason => console.log(reason)) // 'reason'
class PromiseImpl {
  then(onFulfilled, onRejected) {
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }

    // ...其他程式碼
  }
}

2.3 Promise 解決過程

Promise 解決過程是一個抽象的操作,輸入一個 promise 和一個值,這裡我們將其命名為 promiseResolutionProcedure(promise, x, resolve, reject),並在呼叫時傳入 resolvereject 兩個方法, 分別用於在 fulfilledrejected 時呼叫。

2.3.1 如果 promisex 為同一個物件

如果 promisex 為同一個物件,拒絕該 promise,原因為 TypeError

const promiseResolutionProcedure = (promise, x, resolve, reject) => {
  // 2.3.1 If `promise` and `x` refer to the same object,
  // reject `promise` with a `TypeError` as the reason
  if (promise === x) {
    return reject(new TypeError('`promise` and `x` refer to the same object, see: https://promisesaplus.com/#point-48'))
  }

  // ...其他程式碼
}

2.3.2 如果 x 是一個 Promise 物件

如果 x 是一個 Promise 物件,則需要遞迴執行:

const promiseResolutionProcedure = (promise, x, resolve, reject) => {
  // ...其他程式碼

  // 2.3.2 If `x` is a promise, adopt its state:
  //   2.3.2.1 If `x` is pending, `promise` must remain pending until `x` is fulfilled or rejected.
  //   2.3.2.2 If/when `x` is fulfilled, fulfill `promise` with the same value.
  //   2.3.2.3 If/when `x` is rejected, reject `promise` with the same reason.
  if (x instanceof PromiseImpl) {
    return x.then(
      value => promiseResolutionProcedure(promise, value, resolve, reject),
      reason => reject(reason)
    )
  }
}

2.3.3 如果 x 是一個物件或函式

如果 x 是一個物件或函式:

2.3.3.1x.then 賦值給 x

解釋:這樣做有兩個目的:

  • 避免對 x.then 的多次訪問(這也是日常開發中的一個小技巧,當要多次訪問一個物件的同一屬性時,通常我們會使用一個變數將該屬性儲存起來,避免多次進行原型鏈查詢)
  • 執行過程中 x.then 的值可能被改變
const promiseResolutionProcedure = (promise, x, resolve, reject) => {
  // ...其他程式碼

  // 2.3.3 Otherwise, if x is an object or function
  if ((x !== null && typeof x === 'object') || typeof x === 'function') {
    
    // 2.3.3.1 Let `then` be `x.then`
    let then = x.then
  }
}

2.3.3.2 如果在對 x.then 取值時丟擲異常 e,則拒絕該 promise,原因為 e

const promiseResolutionProcedure = (promise, x, resolve, reject) => {
  // ...其他程式碼

  if ((x !== null && typeof x === 'object') || typeof x === 'function') {
    
    try {
      // 2.3.3.1 Let `then` be `x.then`
    } catch (e) {
      // 2.3.3.2 If retrieving the property `x.then` results in a thrown exception `e`,
      // reject `promise` with `e` as the reason.
      reject(e)
    }
  }
}

2.3.3.3 如果 then 是函式

如果 then 是函式,則將 x 作為 then 的作用域,並呼叫 then,同時傳入兩個回撥函式,第一個名為 resolvePromise,第二個名為 rejectPromise

解釋:意思就是在呼叫 then 的時候要指定其 this 值為 x,同時需要傳入兩個回撥函式。這時候用 call() 來實現是最好不過了:

then.call(x, resolvePromise, rejectPromise)

2.3.3.3.1 ~ 2.3.3.3.4 總結起來的意思如下:

  • 2.3.3.3.1 如果 resolvePromise 被呼叫,則遞迴呼叫 promiseResolutionProcedure,值為 y。因為 Promise 中可以巢狀 Promise

    then.call(
      x,
      y => promiseResolutionProcedure(promise, y, resolve, reject),
      rejectPromise
    )
  • 2.3.3.3.2 如果 rejectPromise 被呼叫,引數為 r,則拒絕執行 Promise,原因為 r

    then.call(
      x,
      y => promiseResolutionProcedure(promise, y, resolve, reject), // resolvePromise
      r => reject(r) // rejectPromise
    )
  • 2.3.3.3.3 如果 resolvePromiserejectPromise 均被呼叫,或者被同一引數呼叫了多次,則只執行首次呼叫

    解釋:這裡我們可以通過設定一個標誌位來解決,然後分別在 resolvePromiserejectPromise 這兩個回撥函式內做判斷:

    // 初始化時設定為 false
    let called = false
    
    if (called) {
        return
    }
    
    // `resolvePromise` 和 `rejectPromise` 被呼叫時設定為 true
    called = true
  • 2.3.3.3.4 呼叫 then 丟擲異常 e 時,如果這時 resolvePromiserejectPromise 已經被呼叫,則忽略;否則拒絕該 Promise,原因為 e
const promiseResolutionProcedure = (promise, x, resolve, reject) => {
  // ...其他程式碼
  
  let called = false

  if ((x !== null && typeof x === 'object') || typeof x === 'function') {
    try {
      let then = x.then

      if (typeof then === 'function') {
        // 2.3.3.3 If `then` is a function, call it with `x` as `this`,
        // first argument `resolvePromise`, and second argument `rejectPromise`
        then.call(
          // call it with `x` as `this`
          x,

          // `resolvePromise`
          // 2.3.3.3.1 If/when `resolvePromise` is called with a value `y`,
          // run `[[Resolve]](promise, y)`.
          y => {
            // 2.3.3.3.3 If both `resolvePromise` and `rejectPromise` are called,
            // or multiple calls to the same argument are made,
            // the first call takes precedence, and any further calls are ignored.
            if (called) {
              return
            }
            called = true

            promiseResolutionProcedure(promise, y, resolve, reject)
          },

          // `rejectPromise`
          // 2.3.3.3.2 If/when `rejectPromise` is called with a reason `r`,
          // reject `promise` with `r`
          r => {
            // 2.3.3.3.3
            if (called) {
              return
            }
            called = true

            reject(r)
          }
        )
      } else {
        // 2.3.3.4 If `then` is not a function, fulfill `promise` with `x`
        resolve(x)
      }
    } catch (e) {
      // 2.3.3.3.3
      if (called) {
        return
      }
      called = true

      // 2.3.3.2 If retrieving the property `x.then` results in a thrown exception `e`,
      // reject `promise` with `e` as the reason.

      // 2.3.3.3.4 If calling `then` throws an exception `e`
      //   2.3.3.3.4.1 If `resolvePromise` or `rejectPromise` have been called, ignore it
      //   2.3.3.3.4.2 Otherwise, reject `promise` with `e` as the reason

      reject(e)
    }
  }
}

2.3.3.4 如果 then 不是函式,則執行該 promise,引數為 x

const promiseResolutionProcedure = (promise, x, resolve, reject) => {
  // ...其他程式碼

  if ((x !== null && typeof x === 'object') || typeof x === 'function') {
    try {
      let then = x.then

      if (typeof then === 'function') {
        // 2.3.3.3
      } else {
        // 2.3.3.4 If `then` is not a function, fulfill `promise` with `x`
        resolve(x)
      }
    } catch (e) {
    }
  }
}

2.3.4 如果 x 既不是物件也不是函式

如果 x 既不是物件也不是函式,執行該 promise,引數為 x

const promiseResolutionProcedure = (promise, x, resolve, reject) => {
  // ...其他程式碼

  if ((x !== null && typeof x === 'object') || typeof x === 'function') {
     // 2.3.3
  } else {
    // 2.3.4 If `x` is not an object or function, fulfill `promise` with `x`
    resolve(x)
  }
}

至此,我們的自定義 Promise 已經完成。這是原始碼:promise-aplus-implementing

4. 如何測試

Promise/A+ 規範提供了一個測試指令碼:promises-tests,你可以用它來測試你的實現是否符合規範。

PromiseImpl.js 裡新增以下程式碼:

// PromiseImpl.js

const STATUS_PENDING = 'pending'
const STATUS_FULFILLED = 'fulfilled'
const STATUS_REJECTED = 'rejected'

const invokeArrayFns = (fns, arg) => {
  // ...相關程式碼
}

const promiseResolutionProcedure = (promise, x, resolve, reject) => {
  // ...相關程式碼
}

class PromiseImpl {
  // ...相關程式碼
}

PromiseImpl.defer = PromiseImpl.deferred = () => {
  const dfd = {}
  dfd.promise = new PromiseImpl((resolve, reject) => {
    dfd.resolve = resolve
    dfd.reject = reject
  })

  return dfd
}

module.exports = PromiseImpl

然後在 package.json 裡新增 scripts

// package.json

{
  "scripts": {
    "test": "promises-aplus-tests PromiseImpl.js"
  }
}

然後,執行 npm run test,執行測試指令碼,如果你的實現符合 Promise/A+ 規範的話,所有測試用例均會通過。

按照 Promise/A+ 規範逐行註釋並實現 Promise

5. 其他 API 的實現

Promise/A+ 規範只規定了 then() 方法的實現,其他的如 catch()finally() 等方法並不在規範裡。但就實現而言,這些方法可以通過對 then() 方法或其他方式進行二次封裝來實現。

另外,像是 all()race() 等這些方法,其引數為一個可迭代物件,如 ArraySetMap 等。那麼,什麼是可迭代物件根據 ES6 中的規範,要成為可迭代物件,一個物件必須實現 @@iterator 方法,即該物件必須有一個名為 @@iterator 的屬性,通常我們使用常量 Symbol.iterator 來訪問該屬性。

根據以上資訊,判斷一個引數是否為可迭代物件,其實現如下:

const isIterable = value => !!value && typeof value[Symbol.iterator] === 'function'

Promise.resolve

Promise.resolve(value)靜態方法,其引數有以下幾種可能:

  • 引數是 Promise 物件
  • 引數是 thenable 物件(擁有 then() 方法的物件)
  • 引數是原始值或不具有 then() 方法的物件
  • 引數為空

因此,其返回值由其引數決定:有可能是一個具體的值,也有可能是一個 Promise 物件:

class PromiseImpl { 
  static resolve(value) {
    return new PromiseImpl((resolve, reject) => resolve(value))
  }
}

Promise.reject

Promise.reject(reason)靜態方法,引數為 Promise 拒絕執行時的原因,同時返回一個 Promise 物件,狀態為 rejected

class PromiseImpl { 
  static reject(reason) {
    return new PromiseImpl((resolve, reject) => reject(reason))
  }
}

Promise.prototype.catch

Promise.prototype.catch(onRejected) 其實就是 then(null, onRejected) 的語法糖:

class PromiseImpl {
  catch(onRejected) {
    return this.then(null, onRejected)
  }
}

Promise.prototype.finally

顧名思義,不管 Promise 最後的結果是 fulfilled 還是 rejectedfinally 裡的語句都會執行:

class PromiseImpl {
  finally(onFinally) {
    return this.then(
      value => PromiseImpl.resolve(onFinally()).then(() => value), 
      reason => PromiseImpl.resolve(onFinally()).then(() => { throw reason })
    )
  }
}

Promise.all

Promise.all(iterable)靜態方法,引數為可迭代物件:

  • 只有當 iterable 裡所有的 Promise 都成功執行後才會 fulfilled,回撥函式的返回值為所有 Promise 的返回值組成的陣列,順序與 iterable 的順序保持一致。
  • 一旦有一個 Promise 拒絕執行,則狀態為 rejected,並且將第一個拒絕執行的 Promise 的原因作為回撥函式的返回值。
  • 該方法會返回一個 Promise
class PromiseImpl {
  static all(iterable) {
    if (!isIterable(iterable)) {
      return new TypeError(`TypeError: ${typeof iterable} is not iterable (cannot read property Symbol(Symbol.iterator))`)
    }

    return new PromiseImpl((resolve, reject) => {
      // `fulfilled` 的 Promise 數量
      let fulfilledCount = 0
      // 收集 Promise `fulfilled` 時的值
      const res = []
  
      for (let i = 0; i < iterable.length; i++) {
        const iterator = iterable[i]
        iterator.then(
          value => {
            res[i] = value
            fulfilledCount++
            if (fulfilledCount === iterable.length) {
              resolve(res)
            }
          },
          reason => reject(reason)
        )
      }
    })
  }
}

測試一下:

const promise1 = Promise.resolve(42)
const promise2 = new PromiseImpl((resolve, reject) => setTimeout(() => resolve('value2'), 1000))

PromiseImpl.all([
  promise1,
  promise2
]).then(values => console.log('values:', values))

結果:

values: [42, 'value2']

好像挺完美的,但是事實果真如此嗎?仔細看看我們的程式碼,假如 iterable 是一個空的陣列呢?假如 iterable 裡有不是 Promise 的呢?就像這樣:

PromiseImpl.all([])
PromiseImpl.all([promise1, promise2, 'value3'])

這種情況下執行前面的程式碼,是得不到任何結果的。因此,程式碼還要再改進一下:

class PromiseImpl {
  static all(iterable) {
    if (!isIterable(iterable)) {
      return new TypeError(`TypeError: ${typeof iterable} is not iterable (cannot read property Symbol(Symbol.iterator))`)
    }

    return new PromiseImpl((resolve, reject) => {
      // `fulfilled` 的 Promise 數量
      let fulfilledCount = 0
      // 收集 Promise `fulfilled` 時的值
      const res = []
      
      // - 填充 `res` 的值
      // - 增加 `fulfilledCount`
      // - 判斷所有 `Promise` 是否已經全部成功執行
      const processRes = (index, value) => {
        res[index] = value
        fulfilledCount++
        if (fulfilledCount === iterable.length) {
          resolve(res)
        }
      }

      if (iterable.length === 0) {
        resolve(res)
      } else {
        for (let i = 0; i < iterable.length; i++) {
          const iterator = iterable[i]
          
          if (iterator && typeof iterator.then === 'function') {
            iterator.then(
              value => processRes(i, value),
              reason => reject(reason)
            )
          } else {
            processRes(i, iterator)
          }
        }
      }
    })
  }
}

現在再來測試一下:

const promise1 = PromiseImpl.resolve(42)
const promise2 = 3
const promise3 = new PromiseImpl((resolve, reject) => setTimeout(() => resolve('value3'), 1000))

PromiseImpl.all([
  promise1,
  promise2,
  promise3,
  'a'
]).then(values => console.log('values:', values))
// 結果:values: [42, 3, 'value3', 'a']

PromiseImpl.all([]).then(values => console.log('values:', values))
// 結果:values: []

Promise.allSettled

Promise.allSettled(iterable)靜態方法,引數為可迭代物件:

  • iterable 裡所有的 Promise 都成功執行或拒絕執行後才完成,返回值是一個物件陣列。
  • 如果有巢狀 Promise,需要等待該 Promise 完成。
  • 返回一個新的 Promise 物件。

對於 allSettled,我們可以在 all 的基礎上進行封裝:

class PromiseImpl {
  static allSettled(iterable) {
    if (!isIterable(iterable)) {
      return new TypeError(`TypeError: ${typeof iterable} is not iterable (cannot read property Symbol(Symbol.iterator))`)
    }

    const promises = iterable.map(iterator => PromiseImpl.resolve(iterator).then(
      value => ({ status: STATUS_FULFILLED, value }),
      reason => ({ status: STATUS_REJECTED, reason })
    ))

    return PromiseImpl.all(promises)
  }
}

測試結果:

PromiseImpl.allSettled([
  PromiseImpl.resolve(42),
  PromiseImpl.reject('Oops!'),
  PromiseImpl.resolve(PromiseImpl.resolve(4242)),
  'a'
]).then(values => console.log(values))
// 結果:
// [
//   { status: 'fulfilled', value: 42 },
//   { status: 'rejected', reason: 'Oops!' },
//   { status: 'fulfilled', value: 4242 },
//   { status: 'fulfilled', value: 'a' }
// ]

Promise.race

Promise.race(iterable)靜態方法,引數為可迭代物件:

  • iterable 裡的任一 Promise 成功執行或拒絕執行時,使用該 Promise 成功返回的值或拒絕執行的原因
  • 如果 iterable 為空,則處於 pending 狀態
  • 返回一個新的 Promise 物件

例如:

Promise.race([
  Promise.resolve(42),
  Promise.reject('Oops!'),
  'a'
]).then(values => console.log(values))
  .catch(reason => console.log(reason))
// 結果:42

Promise.race([
  Promise.reject('Oops!'),
  Promise.resolve(42),
  'a'
]).then(values => console.log(values))
  .catch(reason => console.log(reason))
// 結果:Oops!

實現如下:

class PromiseImpl {
  static race(iterable) {
    if (!isIterable(iterable)) {
      return new TypeError(`TypeError: ${typeof iterable} is not iterable (cannot read property Symbol(Symbol.iterator))`)
    }

    return new PromiseImpl((resolve, reject) => {
      if (iterable.length === 0) {
        return
      } else {
        for (let i = 0; i < iterable.length; i++) {
          const iterator = iterable[i]
          
          if (iterator && typeof iterator.then === 'function') {
            iterator.then(
              value => resolve(value),
              reason => reject(reason)
            )
            return
          } else {
            resolve(iterator)
            return
          }
        }
      }
    })
  }
}

這裡要注意一點:別忘了使用 return 來結束 for 迴圈

測試結果:

PromiseImpl.race([
  PromiseImpl.resolve(42),
  PromiseImpl.reject('Oops!'),
  'a'
]).then(values => console.log(values))
  .catch(reason => console.log(reason))
// 結果:42

PromiseImpl.race([
  PromiseImpl.reject('Oops!'),
  PromiseImpl.resolve(42),
  'a'
]).then(values => console.log(values))
  .catch(reason => console.log(reason))
// 結果:'Oops!'

PromiseImpl.race([
  'a',
  PromiseImpl.reject('Oops!'),
  PromiseImpl.resolve(42)
]).then(values => console.log(values))
  .catch(reason => console.log(reason))
// 結果:'a'

6. 共同探討

  1. 2.3.3.1 把 x.then 賦值給 then 中,什麼情況下 x.then 的指向會被改變?
  2. 2.3.3.3 如果 then 是函式 中,除了使用 call() 之外,還有什麼其他方式實現嗎?

7. 總結

實現 Promise,基本分為三個步驟:

  1. 定義 Promise 的狀態
  2. 實現 then 方法
  3. 實現 Promise 解決過程

8. 寫在最後

以前,我在意能不能自己實現一個 Promise,到處找文章,這塊程式碼 Ctrl+C,那塊程式碼 Ctrl+V。現在,我看重的是實現的過程,在這個過程中,你不僅會對 Promise 更加熟悉,還可以學習如何將規範一步步轉為實際程式碼。做對的事,遠比把事情做對重要

如果你覺得這篇文章對你有幫助,還請:點贊、收藏、轉發;如果你有疑問,請在評論區寫出來,我們一起探討。同時也歡迎關注我的公眾號:前端筆記

參考資料