前端面試寶典之:手寫Promise 手動實現一個 promise

手動實現一個 promise

規範

首先,讓我們明確一下,我們要實現的究竟是什麼?
Promise 實際上是一個規範,具體可以參考:Promise/A+
而我們要做的,僅僅是實現規範而已,接下來,讓我們一步一步,從易到難寫出一個簡單的 Promise 實現。

STEP 1 搭建基礎框架

Promise 是一個什麼?
按照我們的使用慣例,這類需要使用 new 關鍵字來生成的,通常是一個類。

class MyPromise {
  constructor() {}
}

這樣,我們有了一個最簡單的 MyPromise 類。
其次,我們要想一想,在構造函數中,我們要怎麼去定義這個類,它的入參是什麼,又有哪些必備的屬性?
以下是我們常用的寫法:

new Promise((resolve, reject) => { ... })

由此可見,入參是一個 function,而且,這個 function 裏還包含了兩個入參,分別爲 resolve 方法和 reject 方法。
此外,根據規範,Promise 的實例應該具備 state(狀態),value(值)和 reason(異常原因)。
另外,它的實例還有 then()方法、catch()方法。(try 和 finally 方法的提案太新,可以暫時不實現)
此外,它還包含四個靜態方法:resolve()、reject()、all()、race()。
知道了這些,我們就可以搭建出一個基本的框架了。

class MyPromise {
  constructor(fn) {
    this.state = undefined // 狀態
    this.value = undefined // 值
    this.reason = undefined // 異常原因
    this.then = (callback) => {} // then 方法
    this.catch = (callback) => {} // catch方法
  }
  // 以下皆爲靜態方法
  static resolve(value){ }
  static reject(value){ }
  static all(promiseArr) { }
  static race(promiseArr) {}
}

如上,就是 MyPromise 的基本骨架了,下一節,就讓我們來實現其中最核心的構造函數,以及.then 方法吧。

STEP 2 核心:構造函數與 then 函數

首先,讓我們給三個狀態定義一下常量,並且初始化一下

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

class MyPromise {
    constructor(fn) {
        this.state = PENDING
        ...
    }
}

接下來,我們需要處理一下入參 fn 的問題。
這個方法,它還包含兩個入參,並且其中的內容,是在實例化時立刻執行的,並且是同步的。請看下面的例子:

new Promise((resolve, reject) => {console.log(1)})
console.log(2)
// 1
// 2

因此,我們代碼如下:

class MyPromise {
  constructor(fn) {
    this.state = PENDING
    this.value = undefined
    this.reason = undefined
    this._resolve = res => { }
    this._reject = err => { }
    fn(this._resolve, this._reject)
    ...
  }
  ...
}

此時,我們需要好好考慮一下,當入參的 fn 中,調用 resolve 方法時,都發生了什麼?
首先,實例的狀態會變,從'pending'變爲'resolved',實例的 value 屬性會被賦值,然後,實例的.then(onFulfilled, onRejected) 中的 onFulfilled 方法會被調用,且 res 這個入參就是 resolve(x) 中返回的值 x。
reject 時同理,不過調用的是 onRejected;
但這裏有個細節需要注意。
那就是 then 方法的回調,是異步的,而狀態的改變卻是同步,這裏我們可以寫個例子看看。

const a = new Promise((resolve, reject) => {resolve(1)}).then(res => console.log(1, a))
console.log(2, a)
// 2 {value: 1, state: 'resolved'}
// 1 {value: 1, state: 'resolved'}

可以看出來,Promise 先執行了同步的內容,才執行的回調中的內容,而且在同步的 console.log(2, a)中,其 state 已經變成了'resolved'。
而且,.then()方法實際返回的夜市一個 Promise 實例,這樣纔可以保證鏈式調用。
因此,此處我們可以簡單地實現爲:

class MyPromise {
  constructor(fn) {
    ...
    this.then = (onFulfilled, onRejected) => {
      this.onRejected = onRejected
      return new MyPromise((resolve, reject) => {
        this.thenMethod = res => {
            // 此處先不考慮.then中返回的是Promise實例的情況
            // 將then中onFulfilled的執行內容先封裝個方法存起來,後期再執行
            const onFulfilledRes = onFulfilled(res)
            resolve(onFulfilledRes)
        }
      })
    }
    this._resolve = res => {
      if (this.state !== PENDING) {
        return
      }
      this.value = res
      this.state = RESOLVED
      setTimeout(() => {
        // 異步地執行.then中被保存起來的,onFulfilledRes的內容
        this.thenMethod && this.thenMethod(res)
      }, 0)
    }
  }
}

這樣,一個最簡單的 Promise 的核心就出來了。
再加上異常處理和捕獲,主體部分就能完成撰寫:

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

class MyPromise {
  constructor(fn) {
    this.state = PENDING
    this.value = undefined
    this.reason = undefined

    this._resolve = res => {
      if (this.state !== PENDING) {
        return
      }
      setTimeout(() => {
        this.value = res
        this.state = RESOLVED
        this.thenMethod && this.thenMethod(res)
      }, 0)
    }
    this.then = (onFulfilled, onRejected) => {
      this.onRejected = onRejected
      return new MyPromise((resolve, reject) => {
        this.thenMethod = res => {
          if (onFulfilled instanceof Function) {
            try {
              const onFulfilledRes = onFulfilled(res)
              if (onFulfilledRes instanceof MyPromise) {
                onFulfilledRes.then(res => {
                  resolve(res)
                })
              } else {
                resolve(onFulfilledRes)
              }
            } catch (err) {
              reject(err)
            }
          }
        }
      })
    }
    this._reject = err => {
      this.state = REJECTED
      this.reason = err
      if (this.onRejected) {
        this.onRejected(err)
        return
      }
      this.onException && this.onException(err)
    }
    this.catch = onException => {
      this.onException = onException
    }
    fn(this._resolve, this._reject)
  }
}

STEP 3 收汁,幾個簡單的靜態方法

衆所周知,es6 的 class 標準,是允許定義靜態方法的,如下:

class A {
static sayHello(){console.log('hello')}
}

A.sayHello()
// hello

靜態方法不屬於某個實例,而是屬於這個類本身,Promise.resolve()、Promise.reject()、Promise.all()、Promise.race()
在已經鋪好了核心代碼的情況下,靜態方法顯得如此輕鬆。

class MyPromise {
    ...
  /**
   * @param {any} value
   * @returns {MyPromise}
   */
  static resolve(value) {
    if (value instanceof MyPromise) {
      return MyPromise
    }
    return new MyPromise((resolve, reject) => {
      resolve(value)
    })
  }

  /**
   * @param {any} value
   * @returns {MyPromise}
   */
  static reject(value) {
    if (value instanceof MyPromise) {
      return MyPromise
    }
    return new MyPromise((resolve, reject) => {
      reject(value)
    })
  }

  /**
   * @param {Iterator} promiseArr
   * @returns {MyPromise}
   * @description
   * 1. 首先解釋下入參,大多數情況下Promise.all的入參是一個數組,數組的每個元素都是Promise實例,
   * 但實際上入參允許是具備Iterator接口的對象,但返回的每個成員都必須是Promise實例
   * 2. 其原則爲, Iterator接口的每個成員, 狀態都爲fulfilled時, 返回的Promise實例變爲fulfilled狀態,且
   * resolve方法入參爲一個數組,數組的元素爲每個Promise實例的返回值。
   * 3. 方法返回的Promise實例的.catch方法, 會拋出Iterator接口中, 第一個變爲rejected狀態的異常值。
   */
  static all(promiseArr) {
    // 先將遍歷體轉換爲數組,然後將非promise元素轉換爲Promis實例
    const MyPromiseArr = Array.from(promiseArr).map(item => {
      if (item instanceof MyPromise) {
        return item
      }
      return MyPromise.resolve(item)
    })
    return new MyPromise((resolve, reject) => {
      let count = 0
      let totle = promiseArr.length
      const resArr = new Array(totle)
      MyPromiseArr.forEach((item, index) => {
        item
          .then(res => {
            resArr[index] = res
            count++
            if (count == totle) {
              resolve(resArr)
            }
          })
          .catch(err => {
            reject(err)
          })
      })
    })
  }

  /**
   * @param {Iterator} promiseArr
   * @returns {MyPromise}
   * @description
   * 1. 首先解釋下入參,大多數情況下Promise.all的入參是一個數組,數組的每個元素都是Promise實例,
   * 但實際上入參允許是具備Iterator接口的對象,但返回的每個成員都必須是Promise實例;(如果不是promise,則轉換成promise)
   * 2. 其原則爲, Iterator接口的每個成員, 第一個狀態都爲fulfilled時, 返回的Promise實例變爲fulfilled狀態,且
   * resolve方法入參爲一個數組,數組的元素爲第一個Promise實例的返回值。
   * 3. 方法返回的Promise實例的.catch方法, 會拋出Iterator接口中, 第一個變爲rejected狀態的異常值。
   */
  static race(promiseArr) {
    const MyPromiseArr = Array.from(promiseArr).map(item => {
      if (item instanceof MyPromise) {
        return item
      }
      return MyPromise.resolve(item)
    })
    return new MyPromise((resolve, reject) => {
      MyPromiseArr.forEach(item => {
        item
          .then(res => {
            resolve(res)
          })
          .catch(err => {
            reject(err)
          })
      })
    })
  }
}

總結

好了,現在,一個完整的 Promise 就被我們手寫出來了。
完整代碼已經上傳了 github,感興趣的朋友可以 down 下來看看,並且歡迎給我一顆寶貴的小星星。
代碼地址
謝謝!

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